本文将从C++的内存管理、内存泄漏、内存回收三个方面研究C++的内存管理问题。
1、 内存管理
C++中,内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
函数内局部变量的存储单元在栈上创建,函数执行结束后这些存储单元被自动释放。栈内存分配运算内置于处理器的指令集中,效率很高。
用new运算符分配的内存块在堆上创建,这些内存的释放由程序员控制。在一个new运算符后一般会跟着一个delete运算符。如果程序员没有手动释放,操作系统会自动回收。
自由存储区是由malloc分配的内存块,相应的可以用free释放分配的内存块。
全局/静态存储区用于存放全局变量和静态变量。
常量存储区存储常量。
实例一:
void f(){int *p=new int[5];}
解释:在栈内存中存放了一个指向一块堆内存的指针p。程序执行时,先判断要分配的内存的大小,然后调用操作符new分配内存,创建成功后返回这块内存的首地址。
堆栈内存的区别:
管理方式不同:栈由编译器自动管理,堆由程序员和操作系统管理。
空间大小不同:堆内存可以达到4G,几乎没有限制。栈内存有限。
碎片问题:频繁的new/delete会造成内存空间不连续,从而造成碎片,程序效率会下降。栈则不会出现碎片问题,后分配的内存块肯定先弹出,不会产生碎片。
生长方向不同:堆内存的生长方式是向上的,向着内存地址增加的方向;对于栈,生长方向是向下的,向着内存地址减小的方向生长。
分配方式不同:堆是动态分配的。栈可以进行动态分配和静态分配。静态变量的分配是编译器自动进行的,比如局部变量的分配。动态分配是由malloc函数进行分配,不同的是栈的内存分配是由编译器进行释放的。
分配效率:栈是系统提供的数据结构,有专门的指令对应,执行效率高。堆是由C/C++函数库提供的。可能会引发用户态和核心态的转换。
问题:在内存有限制的情况下,频繁的动态分配不定大小的内存会引起其他的问题和堆破碎的危险。
解决方法:在不同固定大小的内存池中分配不同类型的对象。对每个类重载new和delete。
重载new和delete的代码:
void *operator new(size_t size)
{
void *p=malloc(size);
return (p);
}
void operator delete(void *p)
{
free(p);
}
上述代码代替默认的操作符来满足内存分配的请求。
void *TestClass::operator new(size_t size)
{
void *p = malloc(size); // Replace this with alternative allocator
return (p);
}
void TestClass::operator delete(void *p)
{
free(p); // Replace this with alternative de-allocator
}
所有TestClass对象的内存分配都采用这段代码,任何从TestClass继承的类也可以采用这一方式,从而实现从不同的内存池中分配不同的类对象。
对象数组的内存分配请求会被定向到全局的new[]和delete[],这些内存来自系统堆。
C++将对象数组的内存分配作为一个单独的操作,而不同于单个对象的内存分配。你需要重载全局的new[] delete[]操作符。
void *TestClass::operator new[ ](size_t size)
{
void *p = malloc(size);
return (p);
}
void TestClass::operator delete[ ](void *p)
{
free(p);
}
new[]操作符中的个数参数是数组的大小加上额外存储对象数目的一些字节,size的大小需要考虑清楚。
常见的内存错误及解决方法:
内存分配未成功,却已经被程序使用:在使用该内存返回的指针前进行检查是否为NULL,如果该指针时函数的参数,在函数的入口处用断言来判断是个不错的选择:
assert(p!=NULL)
内存分配成功,但是并未初始化就使用:这类错误有两个原因,第一没有初始化的观念,二是误以为内存的缺省的初值全为0,导致引用初值错误。
内存分配成功并且已经初始化,但是发生了越界错误:严格检查分配的内存的大小。
忘记释放内存,造成内存泄漏:如果程序中存在这种错误,那么该函数每执行一次就丢失一块内存,会造成内存耗尽。动态内存的申请和释放必须配对,程序中malloc和free,new和delete的次数要相同。
释放了内存却继续使用:
1) 程序中的对象调用关系过于复杂,需重新设计数据结构,解决对象管理的混乱场面。
2) 函数的return语句写错,不能返回指向栈内存的指针或者引用,该内存在函数体结束时被自动销毁。
3) 使用free或者delete释放了内存后,没有将指针设置为NULL,导致了野指针的产生。
指针与数组的对比
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12字节
cout<< sizeof(p) << endl; // 4字节
sizeof(a)计算的是数组的容量,而sizeof(p)计算的是指针变量的字节数,相当于sizeof(char *),而不是p所指的内存容量。
有一个例外,当数组作为函数参数进行传递时,该数组自动退化为同类型的指针,看下列程序段:
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4字节而不是100字节
}
此时sizeof(a)相当于sizeof(char *)。
指针参数是如何传递内存的:
如果函数的参数是一个指针,不要期望用该指针去申请动态内存。如:GetMemory(str,200)并没有使str获得期望的内存,str的依旧是NULL。
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然为 NULL
strcpy(str, "hello"); // 运行错误
}
错误分析:编译器总要为函数的每个参数制作临时副本,指针参数的副本是_p,编译器使_p=p。如果函数体内修改了_p的内容,就导致参数的p的内容做相应的修改。这就是指针可以作为输出参数的原因,本例中_p申请了新内存,只是把_p所指向的内存地址改变,p的所指向的地址并未变化。由于没有用free释放,该函数还会导致内存泄漏。
要实现用指针参数去申请内存,可以使用指向指针的指针,例如:
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
char *str = NULL;
GetMemory2(&str, 100); // 注意参数是 &str,而不是str
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
比较简单的方法是用函数返回值来传递动态内存。实例如下:
char *GetMemory3(int num)
{
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
void Test3(void)
{
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
此时注意return的写法,不要返回指向栈内存的指针。
char *GetString(void)
{
char p[] = "hello world";
return p; // 编译器将提出警告
}
void Test4(void)
{
char *str = NULL;
str = GetString(); // str 的内容是垃圾
cout<< str << endl;
}
p[]是函数内的局部变量,函数执行结束后,这个内存去会自动释放,所以str指向的内从是未知的。
char *GetString2(void)
{
char *p = "hello world";
return p;
}
void Test5(void)
{
char *str = NULL;
str = GetString2();
cout<< str << endl;
}
2、 内存泄漏
3、 内存回收