Ⅲ. new 和 delete 的底层探索
0x00 operator new 与 operator delete 函数
/* operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败, 尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。 */ 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); } /* operator delete: 该函数最终是通过free来释放空间的 */ 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); __FINALLY _munlock(_HEAP_LOCK); /* release other threads */ __END_TRY_FINALLY return; } /* free的实现 */ #define free(p) _free_dbg(p, _NORMAL_BLOCK)
通过上述两个全局函数的实现可以知道:
① operator new 实际上也是通过 malloc 来申请空间的。
② operator delete 最终也是通过 free 来释放空间的。
如果 malloc 申请空间成功就直接返回,否则执行用户提供的空间不足的应对措施,
如果用户提供该措施就继续申请,否则就抛异常。
面向过程的语言处理错误的方式:
返回值 + 错误码解决(这个我们之前学过)。
#include <stdio.h> #include <stdlib.h> int main(void) { char* p1 = (char*)malloc(1024u * 1024u * 1024u *2u); if (p1 == nullptr) { printf("%d\n", errno); perror("malloc fail"); exit(-1); } else { printf("%p\n", p1); } return 0; }
而面向对象语言处理错误的方式:
一般是抛异常,C++中也要求出错抛异常 —— try catch(后期会细说)。
#include <iostream> using namespace std; int main(void) { char* p2 = nullptr; try { char* p2 = new char[1024u * 1024u * 1024u * 2u - 1]; } catch (const exception& e) { cout << e.what() << endl; } printf("%p\n", p2); return 0; }
🔺 C++ 提出 new 和 delete,主要是解决两个问题:
① 自定义类型对象自动申请的时候,初始化合清理的问题。new / delete 会调用构造函数和析构函数。
② new 失败了以后要求抛异常,这样才符合面向语言的出错处理机制。
(delete 和 free 一般不会失败,如果失败了,都是释放空间上存在越界或者释放指针位置不对)
0x02 operator new 与 operator delete 的类专属重载
下面代码演示了,针对链表的节点 ListNode 通过重载类专属 operator new / operator delete,
实现链表节点使用内存池申请和释放内存,提高效率。
💬 我们先看看按照C的方式写:
struct ListNode { ListNode* _next; ListNode* _prev; int _val; }; int main(void) { struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode)); if (n1 == NULL) { printf("malloc failed!\n"); exit(-1); } n1->_next = NULL; n1->_prev = NULL; n1->_val = 0; return 0; }
我们创建节点还需要用 malloc 申请空间,还需要强制类型转换,之后还要自己写上初始化,
因为 malloc 失败返回 NULL,会存在野指针隐患,所以出于安全还要检查一下。
💬 我们再来看看 C++ 的方式:
struct ListNode { ListNode* _next; ListNode* _prev; int _val; /* 构造函数*/ ListNode(int val) : _next(nullptr) , _prev(nullptr) , _val(val) {} }; int main(void) { ListNode* n2 = new ListNode(0); return 0; }
而在C++里,因为 new 会自动调用构造函数去完成初始化,就很舒服。
而且还不需要去检查是否开辟失败,因为 new 失败不会返回空,而是抛异常。
💬 我们再举个能用得上析构函数的例子 —— Stack:
#include <iostream> using namespace std; class Stack { public: Stack(int capacity = 4) : _top(0) , _capacity(capacity) { _arr = new int[capacity]; } ~Stack() { delete[] _arr; _arr = nullptr; _capacity = _top = 0; } // ... private: int* _arr; int _top; int _capacity; }; int main(void) { Stack st; // 完事了 Stack* pst2 = new Stack; // 开空间 + 构造函数初始化 delete pst2; // 析构函数(清理对象中资源)+ 释放空间 return 0; }
Ⅳ. new 和 delete 的实现原理
0x00 对于内置类型
如果申请的是内置类型的空间,new 和 malloc,delete 和 free 基本相似。
不同的地方是,new / delete 申请和释放的是单个元素的空间,
new[] 和 delete[] 申请的是连续空间。而且 new 再申请空间失败时会抛异常。
A* p3 = new A; // 开辟单个空间 A* p4 = new A[5]; // 开辟的是连续地5个空间
operator new 和 operator delete 就是对 malloc 和 free 的封装。
operator new 中调用 malloc 后申请内存,失败以后,改为抛异常处理错误,
这样符合C++面向对象语言处理错误的方式。
new Stack call malloc + call Stack 构造函数 ❌ 如果失败返回0,这不符合C++处理错误的方式 new Stack call operator new + call Stack 构造函数 ✅ 失败抛异常,这就非常滴合适
0x01 对于自定义类型
new 的原理:
① 调用 operator new 函数申请空间。
② 在申请空间上执行构造函数,完成对象的构造。
delete 的原理:
① 在空间上执行析构函数,完成对象中资源的清理工作。
② 调用 operator delete 函数释放对象的空间。
new T[N] 的原理:
① 调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成 N 个对象空间的申请。
② 在申请的空间上调用 N 次构造函数,对它们分别初始化。
Stack* p1 = new[10]; Stack* pst1 = (Stack*)operator new[](sizeof(Stack) * 10);
delete[] 的原理:
① 在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源的清理。
② 调用 operator delete[] 释放空间,实际在 operator delete[] 中调用 operator delete 来释放空间。
Ⅴ. 定位new
0x00 引入 - 我想手动初始化
如果不用 new,我想手动调用构造函数初始化,
假设我们这有一块空间,是从内存池取来的,或者是 malloc 出来的、operator new 出来的……
我就不想用 new,但是我想对他进行初始化,行不行?
A* p = (A*)malloc(sizeof(A)); // 我能不能调用构造函数初始化?
当然可以!定位new表达式帮你!
0x01 定位new表达式
定位 new 表达式实在已分配的原始空间中调用构造函数初始化一个对象。
简单来说就是,定位new表达式可以在已有的空间进行初始化。
📚 写法:
new(目标地址指针)类型 // 不带参 new(目标地址指针)类型(该类型的初始化列表) // 带参
📌 注意:目标地址必须是一个指针
0x02 定位new的使用场景
定位 new 是很有用的!
比如开的空间是从内存池来的,如果想初始化,我们就可以使用它。
因为内存池分配出的内存初始化,所以如果是自定义类型的对象,
需要使用 new 定义的表达式进行显示调用构造函数进行初始化。
0x03 定位new用法演示
不带参定位new:
class A { public: A(int a = 0) : _a(a) { cout << "A(): " << this << endl; } ~A() { cout << "~A(): " << this << endl; } private: int _a; }; int main(void) { A* p = (A*)malloc(sizeof(A)); new(p)A; // 定位new return 0; }
带参定位new:
class A { public: A(int a) : _a(a) { cout << "A(): " << this << endl; } ~A() { cout << "~A(): " << this << endl; } private: int _a; }; int main(void) { A* p1 = (A*)malloc(sizeof(A)); new(p1)A(10); return 0; }
💬 模拟一下 new 的行为:
int main(void) { A* p1 = (A*)malloc(sizeof(A)); new(p1)A(10); // 模拟一下new的行为 A* p2 = new A(2); // 等价于: A* p3 = (A*)operator new(sizeof(A)); new(p3)A(3); return 0; }
没事这么写,其实就是脱裤子放屁,
但是有时候,内存不一定是从堆来的,比如从内存池来的,定位 new 就可以大显神功。
高并发内存池,实现定长内存池的时候就需要使用 定位 new。
析构函数释放
析构函数是可以显式调用的(构造函数不行)
p->~A;
int main(void) { A* p1 = (A*)malloc(sizeof(A)); new(p1)A(1); A* p2 = new A(2); delete p2; // 等价于: A* p3 = (A*)operator new(sizeof(A)); new(p3)A(3); p3->~A; operator delete(p3); return 0; }