深入探索C++对象模型(十)执行期语义学(new和delete)

关于new和delete运算符。

a. 使用new运算符构造对象时, 例如:

[cpp]  view plain  copy
  1. Point3d* origin = new Point3d;  

会被转化为两个操作:分配空间和调用类的构造函数:

[cpp]  view plain  copy
  1. Point3d* origin;  
  2. if(origin = __new(sizeof(Point3d)))  
  3. {  
  4.     origin = Point3d::Point3d(origin);  
  5. }  
同样的,使用delete释放对象时,例如:
[cpp]  view plain  copy
  1. delete origin;  
会被转化为两步操作:调用类的析构函数和释放内存:
[cpp]  view plain  copy
  1. if(0 != origin)  
  2. {  
  3.     Point3d::~Point3d(origin);  
  4.     __delete(origin);  
  5. }  
b. 对于数组的new语义,会有vec_new调用产生一整组对象,例如:
[cpp]  view plain  copy
  1. Point3d* p_array = new Point3d[10];  
通常会被编译为:
[cpp]  view plain  copy
  1. Point3d* p_array;  
  2. p_array = vec_new(0, sizeof(Point3d), 10, &Point3d::Point3d, &Point3d::~Point3d);  
注意针对new p_array[N]产生的对象数组释放时必须调用delete [] p_array. 
c. Placement operator new语义

Placement new是一个预先定义好的重载的new运算符,其作用是在已经申请好的内存上直接构造对象,例如:

[cpp]  view plain  copy
  1. Point2w* ptw = new(arena) Point2w;//arena为已申请内存的地址  
arena指向一块内存区,用以放置产生的Pointw对象。他需要两个参数,其实现很简单,将获得的指针(上例中的arena)所指的地址传回:
[cpp]  view plain  copy
  1. void* operator new(size_tvoid* p)  
  2. {  
  3.     return p;  
  4. }  
类似于:


[cpp]  view plain  copy
  1. Point2w* ptw = (Point2w*) arena;  
但这只是该操作符扩充的一半,另一半是将Point2w的构造函数实施与arena所指的地址上:


[cpp]  view plain  copy
  1. if(0 != ptw)  
  2. {  
  3.     ptw->Point2w::Point2w();  
  4. }  
注意,如果placement new在原已存在一个object的内存上构造新的object:
[cpp]  view plain  copy
  1. Point2w* ptw = new(arena) Point2w;  
  2. //...do something  
  3. ptw = new(arena) Point2w;  

而现有object有一个析构函数,那么改析构函数不会被调用,我们知道,调用delete object会调用该object的析构函数,但是此处不可以这样做,因为,delete不但会调用析构函数,而且会释放内存,那样arena就不能继续使用了,所以我们需要做的是仅仅调用object的析构函数:

[cpp]  view plain  copy
  1. ptw->~Point2w();  

注:标准C++提供了placement operator delete,它会调用析构函数而不释放内存。

为什么new/delete和new[]/delete[]必须配对使用?

new和delete的内部机制这里不赘述了,戳这里《浅谈 C++ 中的 new/delete 和 new[]/delete[]》 
glibc的mallc和free实现的内存分配释放简介,戳这里《malloc和free的内存到底有多大?——GNU glib库》 
第一篇博客讲的很好,但是最后new、delete的为什么配对使用的解释单纯理解还不到位。这里总结并补充说明一下。


动态内存使用表

分配方式 删除方式 结果 结果分析
new delete 成功 合法
new delete[] 失败 参考下文Q1
new[] delete 内嵌类型成功;自定义类型失败 参考下文Q2
new[] delete[] 成功 合法

合法的new/delete和mallc/free示意图

这里写图片描述 
说明:如果对象按照字节对齐的话,那么对象之间可能存在填充,因此图中的对象间隔不一定存在。

前提知识1:第一篇博客中已经阐明:new [] 时多分配 4 个字节,用于存储用户实际分配的对象个数。而new不会多分配。 
前提知识2:在第二篇博客中,介绍了mallc和free的实际大小,这个信息存储在内存块的块头里面。其中最重要的就是指示实际分配的内存大小(单位:字节),那么在free时,就要将用户的传入的地址,减去块头长度找到实际分配内存的起始地址然后释放掉。块头的长度是8字节。 
知道这两个前提知识下面两个问题就好解释了。

Q1: new为什么使用delete[]失败?

new时不会偏移4字节,delete[]时,编译器就是通过标识符[]而执行减4字节操作。从上图可知,减后的地址值会落到块头中,同时编译器从块头中提取4字节中的值作为自己执行析构对象的个数,而这4个字节是实际内存的长度,这是一个比较偏大的值,比如256,然后编译器对随后的内存对象执行析构,基本上都会导致内存越界,这必然失败。

Q2:new[]为什么内嵌类型使用delete成功,自定义类型delete失败?

new[],如果是内嵌类型,比如char或者int等等,就是C数组,那么它不会向后(地址变大的方向)偏移4个字节。因此执行delete时,显然不会出现任何问题。 
但是如果是自定义类型呢?那么new[]时就会向后偏移4个字节,从malloc的返回地址偏移4个字节用来存储对象个数,如果使用delete,编译器是识别不出释放的是数组,那么它会直接将传入对象的首地址值处执行一次对象析构,这个时候还不会出现问题,但是再进一步,它把对象的首地址值传递给free时,那么这个地址值并不是malloc返回的地址,而是相差了4个字节,此时free向前偏移取出malloc的实际长度时,就会取出对象的个数值作为实际的分配长度进行释放,显然这将导致只释放了n字节,其余的块头一部分和除n字节的所有内存都泄露了,并且只有第一个对象成功析构,其余都没有析构操作。一般对象个数n是个非常小的值,比如128个对象,那么free只释放了128字节。(注意:不同的libc实现不同,这里只示例阐述原理,不深究数字) 
这里写图片描述



说起new和delete,了解过c++的人应该都知道吧,它是用来分配内存和释放内存的两个操作符。与c语言中的malloc和free类似。

c语言中使用malloc/calloc/realloc/free进行动态内存分配,malloc/calloc/realloc用来在堆上分配空间,free将申请的空间释放掉。

malloc:

[cpp]  view plain  copy
  1. void FunTest()  
  2. {  
  3.     int *pTest = (int*)malloc(10*sizeof(int));         //开辟10个int型的空间大小  
  4.     if(pTest != NULL)  
  5.     {  
  6.         free(pTest);  
  7.         pTest = NULL;  
  8.     }  
  9. }  
calloc:

[cpp]  view plain  copy
  1. void FunTest()  
  2. {  
  3.     int *pTest = (int*)calloc(10,sizeof(int));    //分配10个int型的内存块,并将其初始化为0  
  4.     if(pTest != NULL)  
  5.     {  
  6.         free(pTest);  
  7.         pTest = NULL;  
  8.     }  
  9. }  
realloc:

[cpp]  view plain  copy
  1. void FunTest()  
  2. {  
  3.     int *pTest = (int*)malloc(10*sizeof(int));     
  4.     realloc(pTest,20*sizeof(int));   //改变原有空间大小,若不能改变则会新开辟一段空间,并将原有空间的内容                                               拷贝过去,但不会对新开辟的空间进行初始化  
  5.     free(pTest);  
  6. }  
这里要注意的一点是,为什么分配了空间之后,必须要用户手动去free掉呢,是因为malloc、calloc、realloc都是在堆上分配的,堆上分配的空间必须由用户自己来管理,如果不释放,就会造成内存泄漏。而栈上分配的空间是由编译器来管理的,具有函数作用域,出了函数作用域后系统会自动回收,不由用户管理,所以不用用户显式释放空间。

对于内存泄漏,我介绍一下我所见过的内存泄漏吧:

(1)申请内存但并未释放。

[cpp]  view plain  copy
  1. void FunTest()  
  2. {  
  3.     int *pTest1 = (int*)malloc(10*sizeof(int));  
  4.     *pTest1 = 0;  
  5. }  
(2)程序逻辑错误,这里引出两个问题。

①同一块空间释放两次,导致崩溃;

②有一块空间没有释放,以为释放了,导致内存泄漏。

[cpp]  view plain  copy
  1. void FunTest()  
  2. {  
  3.     int *pTest1 = (int*)malloc(10*sizeof(int));  
  4.     int *pTest2 = (int*)malloc(10*sizeof(int));  
  5.   
  6.     pTest1 = pTest2;  
  7.     free(pTest1);  
  8.     free(pTest2);  
  9. }  
(3)程序的误操作,将堆破坏。申请的空间不足以赋值,释放导致崩溃。

[cpp]  view plain  copy
  1. void FunTest()  
  2. {  
  3.     char *pTest1 = (char*)malloc(5);  
  4.     strcpy(pTest1,"hello world");  
  5.     free(pTest1);  
  6. }  
(4)当释放时传入的地址和分配时的地址不一样时,会导致崩溃。
[cpp]  view plain  copy
  1. void FunTest()  
  2. {  
  3.     int *pTest1 = (int*)malloc(10*sizeof(int));  
  4.     assert(pTest1 != NULL);  
  5.     pTest1[0] = 0;  
  6.     pTest1++;      //地址向后移动了一位  
  7.     free(pTest1);  
  8. }  
上述简单的介绍了一下c语言中动态内存管理的类型,下面讲解一下c++中的动态内存管理。

c++中是通过new和delete操作符进行动态内存管理的。

先用一张图简单的说明一下new和delete的含义:


记住:new和delete就像malloc和free一样,都要成对使用哦

我们再看一个这样的表达式:

[cpp]  view plain  copy
  1. string *s = new string("a value");     //分配并初始化一个string对象  
  2. string *str = new string[10];          //分配10个默认初始化的string对象  
这两个new表达式,一个是分配一个对象,一个是分配对象数组。内部实现也是截然不同。

这是string *s = new string("a value"); 这句表达式内部的实现:



我们可以看出new内部的调用顺序:(初始化一个对象时)


new内部的调用顺序:(初始化若干个对象时)


同样地,delete对象时,调用顺序为:(delete单个对象时)



delete对象时,调用顺序为:(delete多个对象时)



接下来,看一下动态内存分布图:


new和delete与malloc和free一样,都是存在堆上的。那么,二者有什么差别呢?


· 总结new/delete和malloc/free的区别和联系:

1. 它们都是动态管理内存的入口。

2. malloc/free是C/C++标准库的函数,new/delete是C++操作符。

3. malloc/free只是动态分配内存空间/释放空间。而new/delete除了分配空间还会调用构造

析构函数进行初始化与清理(清理成员)。

4. malloc/free需要手动计算类型大小且返回值为void*,new/delete可自己计算类型的大小

对应类型的指针。

5.new/delete的底层调用了malloc/free。

6.malloc/free申请空间后得判空,new/delete则不需要。

7.new直接跟类型,malloc跟字节数个数。


猜你喜欢

转载自blog.csdn.net/coolwriter/article/details/80558410