关于new和delete运算符。
a. 使用new运算符构造对象时, 例如:
1. Point3d* origin = new Point3d;
会被转化为两个操作:分配空间和调用类的构造函数:
1. Point3d* origin;
2. if(origin = __new(sizeof(Point3d)))
3. {
4. origin = Point3d::Point3d(origin);
5. }
同样的,使用delete释放对象时,例如:
1. delete origin;
会被转化为两步操作:调用类的析构函数和释放内存:
1. if(0 != origin)
2. {
3. Point3d::~Point3d(origin);
4. __delete(origin);
5. }
b. 对于数组的new语义,会有vec_new调用产生一整组对象,例如:
1. Point3d* p_array = new Point3d[10];
通常会被编译为:
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运算符,其作用是在已经申请好的内存上直接构造对象,例如:
1. Point2w* ptw = new(arena) Point2w;//arena为已申请内存的地址
arena指向一块内存区,用以放置产生的Pointw对象。他需要两个参数,其实现很简单,将获得的指针(上例中的arena)所指的地址传回:
1. void* operator new(size_t, void* p)
2. {
3. return p;
4. }
类似于:
1. Point2w* ptw = (Point2w*) arena;
但这只是该操作符扩充的一半,另一半是将Point2w的构造函数实施与arena所指的地址上:
1. if(0 != ptw)
2. {
3. ptw->Point2w::Point2w();
4. }
注意,如果placement new在原已存在一个object的内存上构造新的object:
1. Point2w* ptw = new(arena) Point2w;
2. //...do something
3. ptw = new(arena) Point2w;
而现有object有一个析构函数,那么改析构函数不会被调用,我们知道,调用delete object会调用该object的析构函数,但是此处不可以这样做,因为,delete不但会调用析构函数,而且会释放内存,那样arena就不能继续使用了,所以我们需要做的是仅仅调用object的析构函数:
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:
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:
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:
1. void FunTest()
2. {
3. int *pTest = (int*)malloc(10*sizeof(int));
4. sizeof(int)); //改变原有空间大小,若不能改变则会新开辟一段空间,并将原有空间的内容 拷贝过去,但不会对新开辟的空间进行初始化
5. free(pTest);
6. }
这里要注意的一点是,为什么分配了空间之后,必须要用户手动去free掉呢,是因为malloc、calloc、realloc都是在堆上分配的,堆上分配的空间必须由用户自己来管理,如果不释放,就会造成内存泄漏。而栈上分配的空间是由编译器来管理的,具有函数作用域,出了函数作用域后系统会自动回收,不由用户管理,所以不用用户显式释放空间。
对于内存泄漏,我介绍一下我所见过的内存泄漏吧:
(1)申请内存但并未释放。
1. void FunTest()
2. {
3. int *pTest1 = (int*)malloc(10*sizeof(int));
4. *pTest1 = 0;
5. }
(2)程序逻辑错误,这里引出两个问题。
①同一块空间释放两次,导致崩溃;
②有一块空间没有释放,以为释放了,导致内存泄漏。
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)程序的误操作,将堆破坏。申请的空间不足以赋值,释放导致崩溃。
1. void FunTest()
2. {
3. char *pTest1 = (char*)malloc(5);
4. "hello world");
5. free(pTest1);
6. }
(4)当释放时传入的地址和分配时的地址不一样时,会导致崩溃。
1. void FunTest()
2. {
3. int *pTest1 = (int*)malloc(10*sizeof(int));
4. assert(pTest1 != NULL);
5. pTest1[0] = 0;
6. //地址向后移动了一位
7. free(pTest1);
8. }
上述简单的介绍了一下c语言中动态内存管理的类型,下面讲解一下c++中的动态内存管理。
c++中是通过new和delete操作符进行动态内存管理的。
先用一张图简单的说明一下new和delete的含义:
记住:new和delete就像malloc和free一样,都要成对使用哦。
我们再看一个这样的表达式:
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跟字节数个数。