C++中程序存储空间除栈空间和静态区外,每个程序还拥有一个内存池,这部分内存被称为或堆(heap)。程序可以用堆来存储动态分配的对象,即那些在程序运行时创建的对象。动态对象的生存期由程序来控制 ,当动态对象不再使用时,程序必须显式的销毁它们。new操作符就是从自由存储区上为对象动态分配内存空间的。这里的自由存储区可以是堆,或者静态区。
1、new和delete的使用
C++中通过一对运算符new和delete来完成动态内存分配。new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象初始化;delete接受一个动态对象的指针,销毁对象,并释放对应内存。使用示例如下:
1 void Test()
2 {
3 int *pi1 = new int;
4 //pi1指向一个动态分配的4个字节(int型)、未初始化的无名对象;*pi1的值未定义
5 int *pi2 = new int(2);
6 //pi2指向的对象的值是2,动态分配4个字节( 1个 int) 的空间并初始化为2
7 int *pi3 = new int[3]; //动态分配12个字节( 3个 int) 的空间
8 int *pi4 = new int(); //值初始化为0;*pi4为0
9
10 delete pi1;
11 delete pi2;
12 delete [] pi3;
13 delete pi4;
14 }
自由空间分配的内存是无名的,new无法为其分配的对象命名,而是返回一个指向该对象的指针。默认情况下,动态分配的对象是默认初始化的,即内置类型或组合类型的对象的值是未定义的,类类型的对象将用默认构造函数进行初始化。
new和delete、 new[] 和delete[] 一定匹配使用 , 一定匹配使用 , 一定匹配使用 ! ! ! 重要的事说三遍! 否则可能出现内存泄露甚至崩溃的问题。
2、深入探究new和delete、 new[] 和delete[]内部实现机制
通过下面这段代码我们来详细比较一下new和delete、 new[] 和delete[]内部实现
1 class Array 2 { 3 public : 4 Array(size_t size = 10)//构造函数 5 : _size(size) 6 , _a(0) 7 { 8 cout << "Array(size_t size) " << endl; 9 if (_size > 0) 10 { 11 _a = new int[size]; 12 } 13 } 14 ~Array() //析构函数 15 { 16 cout << "~Array() " << endl; 17 if (_a) 18 { 19 delete[] _a; 20 _a = 0; 21 _size = 0; 22 } 23 } 24 private: 25 int*_a; 26 size_t _size; 27 }; 28 void Test() 29 { 30 Array* p1 = (Array*)malloc(sizeof(Array)); 31 Array* p2 = new Array; 32 Array* p3 = new Array(20); 33 Array* p4 = new Array[10]; 34 free(p1); 35 delete p2; 36 delete p3; 37 delete[] p4; 38 } 39 int main() 40 { 41 Test(); 42 getchar(); 43 return 0; 44 }
转到反汇编可以看到,在call指令处调用了operator new:
转到定义处可以看到operator new 的具体原型:
其实在operator new的底层同样调用了malloc分配空间,它先为对象分配所申请的内存空间,然后底层调用构造函数构造对象
再按F10程序来到了构造函数
执行完之后,输出
此时new已经完成了申请空间的任务,且调用构造函数创建了对象。同样,detele的定义如下
而delete是先调用析构函数清除对象。然后调用operator detele释放空间。
按F10跳转到了析构函数,析构之后:
然后空间才被释放:
new []与delete []内部执行过程相同,只是底部调用的是operator new []和operator delete []
Array* p4 = new Array[10];
delete[] p4;
执行这两条语句的时候实际上调用operator new[](10*sizeof(Array)+4)分配大小为10*sizeof(Array)+4空间,其中多的四个字节空间用于存放N(10)这个数字以便于delete中调用析构函数析构对象(调用析构函数的次数),空间申请好了之后调用构造函数创建对象。delete[] p4执行的时候首先取N(10)对象个数,然后调用析构函数析构对象,最后用operator delete[]函数释放空间。
3、关键字之间的匹配使用问题
1 void Test () 2 { 3 // 以下代码没有匹配使用, 会发生什么? 有内 存泄露吗? 会崩 溃吗? 4 int* p4 = new int; 5 int* p5 = new int(3) ; 6 int* p6 = new int[3] ; 7 int* p7 = (int*) malloc(sizeof (int) ) ; 8 delete[] p4 ; 9 delete p5 ; 10 free(p5 ) ; 11 delete p6 ; 12 delete p7 ; 13 }
运行结果:没有崩溃。但是当把int换成自定义类型之后则会出现问题。因为内置类型一般不会调用构造函数和析构函数,而自定义类型会,所以是析构对象的时候出现内存泄漏,导致程序崩溃。虽然弄清了问题,但还是建议任何类型都要匹配使用。
4、定位new表达式
new表达式,默认下把内存开辟到堆区。使用定位new表达式,可以在指定地址区域(栈区、堆区、静态区)构造对象,这好比是把内存开辟到指定区域。
定位new表达式调用 void *operator new(size_t, void *); 分配内存。其常见形式有:
1 new(address) type; 2 new(address) type(initializers); 3 new(address) type[size]; 4 new(address) type[size]{braced initializer list};
address必须是个指针,指向已经分配好的内存。
示例代码:
1 #include <iostream> 2 using namespace std; 3 char addr1[100]; //把内存分配到全局/静态区 4 int main() 5 { 6 char addr2[100]; //把内存分配到栈区 7 char *addr3 = new char[100]; //把内存分配到堆区 8 cout << "addr1 = " << (void*)addr1 << endl; 9 cout << "addr2 = " << (void*)addr2 << endl; 10 cout << "addr3 = " << (void*)addr3 << endl; 11 int *p = nullptr; 12 //把对象构造到静态区 13 p = new(addr1)int; 14 *p = 1; 15 cout << (void*)p << " " << *p << endl; 16 //把对象构造到栈区 17 p = new(addr2)int; 18 *p = 2; 19 cout << (void*)p << " " << *p << endl; 20 //把内存分配到堆区 21 p = new(addr3)int; 22 *p = 3; 23 cout << (void*)p << " " << *p << endl; 24 cin.get(); 25 return 0; 26 }
程序中,首先使用变量或new为对象分配空间,然后通过定位new表达式,完成构造函数的调用,将对象创建在已经被分配好的内存中。
定位new表达式不能调用delete删除 placement new的对象,需要人为的调用对象的析构函数,并且人为的释放掉占用的内存。
1 #include <iostream> 2 #include <new> 3 4 using namespace std; 5 6 const int chunk = 16; 7 8 class Foo 9 { 10 public: 11 int val(){return _val;} 12 Foo(){_val=0;} 13 private: 14 int _val; 15 }; 16 17 int main() 18 { 19 // 预分配内存buf 20 char *buf = new char[sizeof(Foo) * chunk]; 21 22 // 在buf中创建一个Foo对象 23 Foo *pb=new (buf) Foo; 24 // 检查一个对象是否被放在buf中 25 if(pb->val()==0) cout<<"new expression worked!"<<endl; 26 // 这里不存在与定位new表达式匹配的delete表达式,即:delete pb, 其实只是为了 27 // 释放内存的话,我们不需要这样的表达式,因为定位new表达式并不分配内存。 28 // 如果在析构函数中要做一些其他的操作呢?就要显示的调用析构函数。 29 // 当程序不再需要buf时,buf指向的内存被删除,它所包含的任何对象的生命期也就 30 // 都结束了。 31 32 delete[] buf; 33 return 0; 34 }
一句话:定位new表达式用于在已分配的原始空间中调用构造函数初始化一个对象。
5、模拟实现new和delete
1 class Test 2 {}; 3 4 Test* newdelete( ) 5 { 6 Test* p1 = NULL; 7 //1、分配空间 2.利用new的定位表达式显式调用构造函数 8 if (p1 = (Test*)malloc(sizeof(Test))) 9 return p1; 10 else 11 throw bad_alloc(); //内存分配失败时抛异常 12 new(p1)Test; //NEW(P1+I)Test(参数列表); 13 14 //3、析构函数 4、释放空间 15 p1->~Test(); 16 free(p1); 17 } 18 19 Test* newdalete_(size_t N) 20 { 21 Test* p2 = NULL; 22 //1、分配空间 2.显示调用构造函数 23 if (p2 = (Test*)malloc(sizeof(Test)*N + 4)) 24 return p2; 25 else 26 throw bad_alloc(); //内存分配失败时抛异常 27 *((int*)p2) = N; 28 p2 = (Test*)((int*)p2 + 1); 29 for (int i = 0; i < N; ++i) 30 { 31 new(p2 + i)Test; 32 } 33 34 int n = *((int*)p2 - 1); 35 //3、析构函数 4、释放空间 36 for (int i = 0; i < n; ++i) 37 { 38 p2[i].~Test(); 39 //(p1 + 1)->~AA(); //也可以 40 } 41 free((int*)p2 - 1); 42 }
总结:
1. operator new/operator delete operator new[] /operator delete[] 和 malloc/free用法一 样。
2. 他们只负责分配空间/释放空间, 不会调用对象构造函数/析构函数来初始化/清理对象。
3. 实际operator new和operator delete只是malloc和free的一层封装。
【 new作用】 调用operator new分配空间。 调用构造函数初始化对象。
【 delete作用】 调用析构函数清理对象 调用operator delete释放空间
【 new[] 作用】 调用operator new分配空间。 调用N次构造函数分别初始化每个对象。
【 delete[] 作用】 调用N次析构函数清理对象。 调用operator delete释放空间。