C++内存管理
一、内存分配方式
在C++中,内存分成5个区,分别是堆、栈、自由存储区、全局/静态区和常量存储区.
栈:存放函数参数以及局部变量,在出作用域时,将自动被释放.栈内存分配运算内置于处理器的指令集中,效率很高,但分配的内存容量有限.
堆:new分配的内存块(包括数组,类实例等),需delete手动释放.如果未释放,在整个程序结束后,OS会帮你回收掉.
自由存储区:malloc分配的内存块,需free手动释放.它和堆有些相似.
全局/静态区:全局变量(global)和静态变量(static)存于此处.(在以前的C语言中,全局变量又分为初始化的和未初始化的,C++不分)
常量存储区:常量(const)存于此处,此存储区不可修改.
1、堆与栈的区别
void f(){ int *p = new int[5];}
上面一段代码就包含了堆与栈.指针P被分配在了栈中,而new出来的东西则被分配在了堆中,此句可以解释为”在栈中存放了一个指向堆内存的指针p”.(可否理解为:指针p的值是堆内存块的首地址??????)
主要区别:
管理方式不同: 栈是编译器自动管理的,堆需手动释放
空间大小不同: 在32位OS下,堆内存可达到4GB的的空间,而栈就小得可怜.(VC6中,栈默认大小是1M,当然,你可以修改它)
能否产生碎片不同:对于栈来说,进栈/出栈都有着严格的顺序(先进后出),不会产生碎片;而堆频繁的new/delete,会造成内存空间的不连续,容易产生碎片.
生长方向不同:栈向下生长,以降序分配内存地址;堆向上生长,以升序分配内在地址.
分配方式不同:堆动态分配,无静态分配;栈分为静态分配和动态分配,比如局部变量的分配,就是动态分配(alloca函数),函数参数的分配就是动态分配(我想的…).
分配效率不同:栈是系统提供的数据结构,计算机会在底层对栈提供支持,进栈/出栈都有专门的指令,这就决定了栈的效率比较高.堆则不然,它由C/C++函数库提供,机制复杂,堆的效率要比栈低得多.
可以看出,栈的效率要比堆高很多,所以,推荐大家尽量用栈.不过,虽然栈有如此多的好处,但远没有堆使用灵活.
二、控制C++的内存分配
其实C++的内存管理容易而且安全,因为当一个对象消除时,它的析构函数能够安全的释放所有分配的内存.在嵌入式系统中,内存的分配是一个常见问题,保守的使用内存分配是嵌入式环境中的第一原则.
当你需使用new/delete时,一个防止堆破碎的通用方法是从不同固定大小的内存池中分配不同类型的对象(??????).对每个类重载new和delete就提供了这样的控制.
class TestClass{ void *operator new(size_t size); void operator delete(void *p);};void *TestClass::operator new(size_t size){ void *p = malloc(size); return p;}void TestClass::operator delete(void *p){ free(p);}
而对象数组的分配又不同于单个对象的分配,所以你仍需再重载new[]和delete[]操作符.但值得注意的是,对于C++而言,分配对象数组的大小等于数组参数的大小再加上额外的对象数目的一些字节,所以要尽量避免使用对象数组。
class TestClass{ void *operator new[](size_t size); void operator delete[](void *p);};void *TestClass::operator new[](size_t size){ void *p = malloc(size); return p;}void TestClass::operator delete[](void *p){ free(p);}void main(){ TestClass *p = new TestClass[10]; delete[] p;}
三、常见的内存错误及对策
1、内存分配未成功,却使用了它
解决办法:在使用之前检查指针是否为NULL,如果指针p是函数参数,那么在函数入口处assert(p!=NULL).如果是用malloc或new申请的话,应该用if(p==NULL)进行防错处理.
2、内存分配成功,但未初始化就使用它
解决办法:不要嫌麻烦,记得初始化就行了.
3、内存分配成功且已初始化,但操作越过了边界
解决办法:此问题通常出现于循环之中,注意不要多1或少1就行.
4、忘记释放内存
解决办法:含有这个错误的函数每调用一次就丢失一块内存,造成内存耗尽.记得free或delete就行.
5、 释放了内存却继续使用它
有三种情况:
※程序中对象的关系过于复杂,难以搞清哪个对象是否已经释放了内存.※函数中return写错,返回了指向栈中的指针或引用.
※ free或delete后,没有将指针设为NULL,产生”野指针”.
四、指针与数组
C++中,指针和数组有着不少相似的地方,容易让我产生错觉,以为它们是等价的,其实不然。
数组在静态存储区或是栈上被创建,数组名对应着(而不是指向)一块内存,其地址与容量在生命周期内保持
不变。而指针可以随时指向任意类型的内存块,远比数组灵活,但也危险。
char a[] = "hello"; a[0] = 'x'; char *p = "world"; p[0] = 'y'; //试图修改常量字符串,编译器不能发现,执行会报错
杜绝”野指针”
“野指针”不是NULL指针,是指向”垃圾内存”的指针.它的缺省值是随机的,所以它会乱指一气.
产生”野指针”的原因有3种:
1、指针变量没有被初始化;
2、指针被free/delete后被没有设置为NULL;
3、指针操作超越了变量的作用域范围.如下例,p->fun()时,a已经消失。
class A { public: void fun() {} }; void Test() { A *p; { A a; p = &a; //a的生命周期会在出作用域时结束 } p->fun(); //p此时是"野指针"}
五、malloc/free 和new/delete
有了malloc/free为何还需要new/delete呢? malloc/free是标准库函数,而new/delete是运算符,它们都可用于申请/释放动态内存.但对于非基本数据类型(比如类对象)而言, malloc/free无法自动执行对象的构造/析构函数.而new/delete却可以.
malloc
函数malloc的原型: void *malloc(size_t size);
函数malloc的使用:
int *p = (int*)malloc(sizeof(int)*length);//length前是乘号
可见,在使用malloc时需要进行类型转换.而使用sizeof运算符也是良好的代码风格.
new
new内置了sizeof,所以用起来写法更简洁。
注意,使用new创建对象数组时,只能使用对象的无参数构造函数。
如 Obj *o = new Obj[100];
六、内存耗尽怎么办?
解决办法:
1、判断指针是否为NULL,如果是立即返回。
void fun(){ A *a = new A(); if(a==NULL) return;}
2、判断指针是否为NULL,如果是立即终止。
void fun(){ A *a = new A(); if(a==NULL) exit(1);}
提示:不要不忍心使用exit(1),否则会害死OS.所以推荐使用方法2。不过搞笑的是,在32位OS上,永远也不会内存耗尽