在本篇中,将会较为详细的介绍在 Cpp 中的两个新操作符 new 和 delete,将会介绍其中的底层原理,以及这两个操作符的使用方法。其中还介绍了 new/delete 操作符使用的细节,还扩展了一些有关定位 new 表达式的知识点。最后总结了 malloc/free 与 new/delete 的区别。
目录
1. C++中的内存管理方式
1.1 new/delete内置类型
1.2 new/delete自定义类型
1.3 new/delete的其他操作
2. new/delete实现原理
2.1 operator new 与 operator delete 函数
2.2 operator new[] 与 operator delete[] 函数
2.3 new/delete的使用细节
3. 定位new表达式
4. 总结malloc/free与new/delete的区别
1. C++中的内存管理方式
在Cpp中的内存管理方式其实和C语言中的内存管理方式相差无几,在C语言中能使用的内存管理方式在Cpp中同样适用,不过在Cpp中适用两个新操作符 new delete 将 malloc 和 free 这两个函数给取代了。
1.1 new/delete内置类型
以下为 new/delete 对于内置类型的操作,如下:
以上就是对 new 操作的使用,对于 ptr1 来说,我们只是分配了一个 int 类型的空间,对于 ptr2 来说,我们使用 new 分配了一个 int 类型的空间并且将其初始化为 10,对于 ptr3 来说,使用类似数组的形式,给其分配了10个 int 类型的空间,我们还可以像初始化数组一样,给 ptr3 初始化,对于剩下未初始化的数组元素,默认使用0进行初始化。接着使用 delete 函数将其的空间释放。
注:申请和释放单个元素的空间,使用 new 和 delete操作符,申请和释放连续的空间使用 new[] 和 delete[],需要注意的是,我们要将其匹配使用。
1.2 new/delete自定义类型
以下为 new delete 对于自定义类型变量的操作,如下:
如上所示,当我们使用 new 和 delete 操作符时的时候,对于自定义类型,new 和 delete 会默认调用其构造函数和析构函数。而相对比而言,malloc 和 free 就只是简单的申请一份空间和释放一份空间。
1.3 new/delete的其他操作
在C语言创建一个链表的时候,我们经常使用 malloc 来创建链表,现在我们可以使用 new 来创建链表,如下:
struct LinkNode { LinkNode* _next; int _data; // 构造函数 LinkNode(int x= 0) :_next(nullptr) ,_data(x) {} }; // 创建 num 个结点的链表 LinkNode* CreateLinkList(int num) { LinkNode head(-1); LinkNode* tail = &head; int val = 0; printf("Please input the val in order:"); for (int i = 0; i < num; i++) { cin >> val; LinkNode* newnode = new LinkNode(val); tail->_next = newnode; tail = tail->_next; } return head._next; }
该操作相对 malloc 函数来说,方便多了,我们在使用 new 开辟空间的时候,也不需要判断是否申请失败,因为使用 new 开辟的空间,会有编译器自动检测是否申请失败。这样的代码写起来也会更方便。
通过以上的代码编写,其实我们也可以总结出一些关于使用 new delete 的优点:
1. 在创建对象的时候可以对对象进行初始化
2. 不需要检查是否会存在申请失败的情况
3. new 写起来更轻便,不需要计算大小,直接就可以进行申请变量或数组需要的空间
4. new 和 delete 对于自定义类型的变量会调用构造函数和析构函数。
2. new/delete实现原理
接下来我们将探讨 new/delete 的实现原理,不光探讨这两个,我们还会探讨 new[]/delete[] 函数的实现原理。
2.1 operator new 与 operator delete 函数
在Cpp中其实存在两个底层函数(也就是很少使用的函数,其他操作符的底层实现函数)。如下所示:
当我们对我们的程序进行 Debug 的时候,转入反汇编,发现对于操作符 new delete,在底层调用的是 operator new() 和 operator delete() 函数。
对于 operator new() 和 operator delete() 函数的底层实现如下:
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void* p; while ((p = malloc(size)) == 0) if (_callnewh(size) == 0) { // report no memory // 如果申请内存失败了,这里会抛出bad_alloc 类型异常 static const std::bad_alloc nomem; _RAISE(nomem); } return (p); } #define free(p) _free_dbg(p, _NORMAL_BLOCK) void operator delete(void* pUserData) { _CrtMemBlockHeader* pHead; RTCCALLBACK(_RTC_Free_hook, (pUserData, 0)); if (pUserData == NULL) return; _mlock(_HEAP_LOCK); /* block other threads */ __TRY /* get a pointer to memory block header */ pHead = pHdr(pUserData); /* verify block type */ _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)); _free_dbg(pUserData, pHead->nBlockUse); // 调用free函数 __FINALLY _munlock(_HEAP_LOCK); /* release other threads */ __END_TRY_FINALLY return; }
如上所示的代码,对于这两个函数而言,其实底层调用的函数也是 malloc 以及 free 函数,只不过还会多加入一些其他的东西,比如在 operator new 函数中,若调用 new 失败了,那么就会自动的抛异常,函数中的这些举动让我们使用 new 起来也更加轻松。
所以,我们对于 new 和 delete 的实现,可以总结为下图:
对于 new 来说,我们是先调用 operator new 函数开辟空间,然后调用构造函数,对生成的变量进行构造。而对于析构函数而言,我们要先进行析构,然后在进行调用operator delete 函数去释放空间。因为对于 delete 而言,如果先进行调用 operator delete函数,那么就提前将空间释放了,若我们的析构函数中还需要对变量进行释放空间,那么这个时候就找不到该释放的空间了。
2.2 operator new[] 与 operator delete[] 函数
以上探讨完 new/delete 的原理后,现在我们也要开始探讨 new[]/delete[] 的实现原理了。既然 new/delete 和 operator new 与 operator delete 函数有关,那么 new[]/delete[] 也许会和 operator new[] 与 operator delete[] 函数 有关,以下将探讨这两个函数。 如下:
如上图所示,我们使用 new[] 时,调用的函数为 operator new[] 函数,那么对于这样的一个函数,其中封装的也是 operator new 函数,如下:
那么接下来我们来观察 delete[] ,对于上图:
我们发现在 push 时,push 进的值为 2Ch,十进制也就是 44,但是对于我们的类 A 来说,一个 A 类对象也就4个字节,十个也应该是40,为什么会是44呢,这是因为 delete 的独特机制,如下:
上图中,红框表示 p1 所在内存地址,篮筐则是系统对 p1 多加入的一个值,其值为 a(10),刚好对应了 p1 中元素的个数,这是因为系统在调用 delete[] 操作符的时候,并不知道的该调用多少次析构函数,所以在 p1 的前一个地址处开辟了一个 int 型的空间,用于存储 p1 的个数,便于在调用 delete[] 时,知道应该调用多少次析构函数。
其实对于 delete[] 函数而言,和 new[] 几乎是同样类型的底层逻辑。所以对于 new[] 和 delete[] 而言,其调用的顺序为:
2.3 new/delete的使用细节
以上已经介绍了许多细节,接下来将会进行介绍其中的使用细节:使用 new 要和 delete 配合使用,使用 new[] 要和 delete[] 配合使用,如下:
如上所示,使用两次同样的方式调用函数,用 new[] 和 delete 搭配使用,一个报警告,一个没有报警告。这是因为对于内置类型来说,不会调用析构函数,那么在使用 delete[] 和 delete 其实是差不多的,因为内置类型不会调用析构函数。但是对于内置类型来说,我们在以上已经说过,当使用自定义类型的时候,系统会多申请一块 int 类型的空间,但是通常系统对于数组的使用,使用的是 int 类型之后的空间,若直接使用 delete 函数进行清除空间操作,那么就相当于在一整块空间的中间部分处开始释放空间,这样肯定会导致报错。
但是当我们将类的析构函数给注释掉的时候,又会是怎么样呢?如下图所示:
此时得到的结果表示为正常运行退出,并没有报错。通过调试我们发现,原本要在空间看开辟的一块 int 大小的空间也没有了,这是为什么呢?
这是因为编译器对代码进行了优化(不同的编译器得出的结果可能不同,所以这不是固定答案),我们将析构函数给注释掉了,而编译器默认生成的析构函数也不会做什么,所以对于编译器来说,直接将这一步骤给省略了,因为就算知道要进行多少次析构,实际上也并没有什么用,所以就将原本准备开辟的空间给取消了。这个时候也不会从中间位置开始释放空间,所以就不会报错了。
3. 定位new表达式
对于定位 new 表达式来说,我们先对其的用法进行说明,使用格式如下:
int main() { A* p1 = (A*)operator new(sizeof(A)); // 显示调用构造函数对一块已经有的空间进行初始化 new(p1)A(10); p1->~A(); operator delete(p1); return 0; }
对以上格式总结来说,就是像调用 malloc 函数一样调用 operator new,然后使用 new(place_address)type(initial),其中 new 旁边的括号中为指针,type 表示类,initial 表示初始化的值。对于删除而言,就算先调用对象的析构函数,然后调用 operator delete 函数将其删去空间。但是这样做的意义是什么呢?为什么不直接调用 new 和 delete 呢?
因为:定位 new 表达式在实际中一般配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。我们平时使用的 new 不是将在内存池中申请空间,而是直接在内存中申请空间。
所以,定位 new 表达式相当于 new 的一种特殊使用场景。
4. 总结malloc/free与new/delete的区别
对于 malloc/free 和 new/delete 来说,他们之间的相同点为都是在堆上开辟空间,都需要由我们手动释放。
他们之间的不同点为:
1. malloc/free 是函数,而 new/delete 是操作符
2. malloc 申请的空间不可以进行初始化,而 new 申请的空间可以进行初始化
3. malloc 申请空间时,需要手动计算申请的空间大小,而 new 只需要在其后跟上空间类型即可,若是多个对象,只需要在 [ ] 中指定个数
4. malloc 申请空间是返回 NULL,需要我们自己判断是否申请成功,而 new 申请空间失败时会抛异常,不需要我们自己判断是否申请成功
5. malloc/free 函数只是进行简单的申请空间和清理空间,而 new/delete 申请空间时会调用构造函数,清理空间时会调用析构函数。