C++ new和delete 堆和栈
一、new和delete基本用法
程序开发中内存的动态分配与管理永远是一个让C++开发者头痛的问题,在C中,一般是通过malloc和free来进行内存分配和回收的,在C++中,new和delete已经完全包含malloc和free的功能,并且更强大、方便、安全。
new一般用法:
new 类型 (初值)
用new分配数组空间时不能指定初值。
delete一般用法:
delete [] 指针变量
[]部分是可选的,当释放数组所占内存时必须加[]。当你对一个指针使用 delete,delete 知道是否有数组大小信息的唯一方法就是由你来告诉它。如果你在你使用的 delete 中加入了方括号,delete 就假设那个指针指向的是一个数组。否则,就假设指向一个单一的对象。
int *i = new int; //没有初始值 int *j = new int(100); //初始值为100 int *iArr = new int[3]; //分配具有3个元素的数组 delete i; //释放单个变量所占用的内存 delete j; delete []iArr; //释放数组所占用的内存
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
二、new/delete和malloc/free的区别
对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。
例1:
#include <iostream> #include <malloc.h> using namespace std; class myclass { public: myclass() { i = 1; } void myfoo() { cout << "i = " << i << endl; } private: int i; }; int main() { myclass *p = new myclass; myclass *q = (myclass *)malloc(sizeof(myclass)); p->myfoo(); q->myfoo(); delete p; free(q); return 0; }
程序执行结果为:
i = 1
i = 0
从上例可看出,new调用了类myclass的构造函数,而malloc只是分配了空间,并没有调用构造函数,因此会出现调用q->myfoo()函数时,输出的结果具有随机性。如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。
例2:
#include <iostream> using namespace std; class myclass { public: ~myclass() { cout << "Goodbye" << endl; } }; int main() { myclass *p = new myclass; free(p); return 0; }
上例中,~myclass()为类的析构函数,对象离开作用域或被delete的时候会调用。指针p指向了一个堆上创建的myclass对象,若用free来释放内存,则不会调用析构函数,所以上面的程序没有输出。如将free(p)改为:
delete p
程序执行时将会调用到myclass类的析构函数,输出结果为:
Goodbye
三、new和多维数组
当使用new运算符定义一个多维数组变量或数组对象时,它产生一个指向数组第一个元素的指针,返回的类型保持了除最左边维数外的所有维数。例如:
int *p1 = new int[10];
返回的是一个指向int的指针int*。
int (*p2)[10] = new int[3][10];
new了一个二维数组,去掉最左边那一维[3],剩下int[10],所以返回的是一个指向int[10]这种一维数组的指针int (*)[10]。
int (*p3)[3][10] = new int[5][3][10];
new了一个三维数组, 去掉最左边那一维[5], 还有int[3][10],所以返回的是一个指向二维数组int[2][10]这种类型的指针int (*)[3][10]。
四、内存分配时的出错处理
我们都知道,使用 malloc/calloc 等分配内存的函数时,一定要检查其返回值是否为“空指针”,即检查分配内存的操作是否成功,这是良好的编程习惯,也是编写可靠程序所必需的。但是,如果你简单地把这一招应用到 new 上,那可就不一定正确了。
例如:
int* p = new int[SIZE]; if ( p = = 0 ) // 检查 p 是否空指针 return -1;
其实,这里的 if( p == 0 )完全是没意义的。在C++里,如果 new 分配内存失败,默认是抛出异常的。所以,如果分配成功,p == 0 就绝对不会成立;而如果分配失败了,也不会执行 if ( p == 0 ),因为分配失败时,new 就会抛出异常跳过后面的代码。如果你想检查 new 是否成功,应该捕捉异常:
try { int* p = new int[SIZE]; } catch ( const bad_alloc& e ) { return -1; }
事实上,C++中并非只有抛出异常的new,也有不抛异常的new,即通常所说的“nothrow new”。可以这样使用它:
T* p = new (nothrow) T(MAX_SIZE);
其中,nothrow是头文件<new>中定义的一个类型为std::nothrow_t的常量,我们可以直接使用它。这时,如果内存分配失败,p的值将为空(0),且不会有异常抛出,跟C的malloc很像了。
四、内存分配的“栈”和“堆”
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
下面通过汇编代码来了解下栈和堆内存的分配:
int main() { int* p = new int; return 0; }
其中,“int* p = new int;
”对应汇编代码为:
0041358E push 4 //分配一个int型数据大小内存(4个字节),相当于call operator new前,参数入栈 00413590 call operator new (4111D6h) 00413595 add esp,4 //call operator new后,恢复栈结构 00413598 mov dword ptr [ebp-0D4h],eax //eax值给call operator new返回的结果生成一个临时变量 0041359E mov eax,dword ptr [ebp-0D4h] //临时变量的值赋给寄存器eax 004135A4 mov dword ptr [p],eax //寄存器eax值赋给栈上指针p
上面这句代码就涉及了内存分配的堆和栈,new分配了一块堆内存,指针p分配的是一块栈内存,这句代码的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,调用结束后返回值存入eax中,再将内存的首地址放入栈中(为p赋值)。
堆和栈主要的区别有以下几点:
(1)管理方式和碎片问题
对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生内存碎片。
(2)分配效率
栈是系统提供的数据结构,计算机会在底层对栈提供支持,分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++提供的,它的机制相对复杂,显然堆的效率比栈要低得多。
下面通过汇编代码分析下栈和堆内存存取的效率。
void main() { char a = 1; char c[] = "1234567890"; char* p = (char *)malloc(10); strcpy(p, c); a = c[1]; a = p[1]; return; }
程序中对堆和栈存取的汇编代码为:
a = c[1]; 004135E7 mov al,byte ptr [ebp-1Fh] 004135EA mov byte ptr [ebp-9],al a = p[1]; 004135ED mov eax,dword ptr [ebp-2Ch] 004135F0 mov cl,byte ptr [eax+1] 004135F3 mov byte ptr [ebp-9],cl
可以看出,在栈上存取时直接就把字符串中的元素读到寄存器al中,在堆上存取时则要先把指针值读到eax中,在根据eax读取字符,显然慢了。
(3)增长方向不同
栈内存由一个栈指针esp来开辟和回收,栈内存是从高地址向低地址增长的,增长时,栈指针向低地址方向移动,指针的地址值也就相应的减小;回收时,栈指针向高地址方向移动,地址值也就增加。所以栈内存的开辟和回收都只是指针的加减。
对于堆来讲,增长方向是向上的,也就是向着内存高地址方向移动;回收时,指针向低地址方向移动,地址值也就减小。
(4)空间大小不同
一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的。无论是堆还是栈,都要防止越界现象的发生,因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果。