在 C++ 中,new和delete是最常用的动态内存管理手段。但说不清它到底做了什么:为什么new会调用构造函数,而malloc不会?operator new和malloc又是什么关系?本文从语法、语义、底层调链三个层次,把这条链路彻底讲清楚。
一、C++ 的内存管理
在讲new和delete之前,先要弄清楚一个程序的内存到底被划分成了哪几块。C++ 程序运行时的内存通常可以划分为以下几个区域:栈区、堆区、全局/静态区、常量区、代码区。不同区域的生命周期、分配方式和访问速度都不一样,理解它们的差异是掌握内存管理的第一步。
1.1 内存分区总览
| 区域 | 存放内容 | 分配/释放方式 | 生命周期 |
|---|---|---|---|
| 栈区(Stack) | 局部变量、函数参数、返回地址 | 编译器自动分配和释放 | 随函数调用结束而销毁 |
| 堆区(Heap) | new/malloc动态申请的内存 | 程序员手动申请和释放 | 从申请到释放为止 |
| 全局/静态区 | 全局变量、静态变量(static) | 程序启动时分配,结束时释放 | 整个程序运行期 |
| 常量区 | 字符串字面量、const全局常量 | 编译期确定,只读 | 整个程序运行期 |
| 代码区 | 函数体的机器指令 | 编译期确定,只读 | 整个程序运行期 |
//内存管理 int globalVar = 1; //全局变量,存放在静态区 static int staticGlobalVar = 1;//存放在静态区 void Test() { static int staticVar = 1;//存放在静态区 int localVar = 1;//存放在栈 int num1[10] = { 1, 2, 3, 4 };//存放在栈 char char2[] = "abcd";//存放在栈,*char2存放在栈 const char* pChar3 = "abcd";//存放在栈,*pChar3存放在常量区 int* ptr1 = (int*)malloc(sizeof(int) * 4);//ptr1存放在栈,*ptr1 存放在堆 int* ptr2 = (int*)calloc(4, sizeof(int)); int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); free(ptr1); free(ptr3); }1.2 从内存分区看 new 与 delete
有了上面的分区概念,再来看new和delete的完整链路就清晰了:new从堆区拿到原始内存,再在这块内存上构造对象;delete先析构对象,再把内存还给堆管理器。
flowchart TD A["new T(args)"] --> B["operator new(sizeof(T))"] B --> C["malloc(size)"] C --> D["操作系统堆 / 内存池"] B --> E["返回 void* 原始内存"] E --> F["在内存上调用 T 的构造函数"] F --> G["返回 T*"] H["delete p"] --> I["调用 T 的析构函数"] I --> J["operator delete(p)"] J --> K["free(p)"]一句话总结:new= 分配内存(operator new → malloc)+ 构造对象;delete= 析构对象 + 释放内存(operator delete → free)。
1.3 C 语言的动态内存:malloc / calloc / realloc
C++ 的operator new底层通常就是调用malloc,所以有必要把 C 的这套动态内存函数讲清楚。它们都声明在<cstdlib>中,操作的都是堆区的原始字节内存,不涉及构造和析构。
| 函数 | 原型 | 是否初始化 | 用途 |
|---|---|---|---|
malloc | void* malloc(size_t size) | 否,内容为随机值 | 申请指定字节数的内存 |
calloc | void* calloc(size_t n, size_t size) | 是,全部清零 | 申请 n 个元素、每个 size 字节,并清零 |
realloc | void* realloc(void* ptr, size_t size) | 新增部分不初始化 | 调整已分配内存的大小 |
malloc:只负责分配,不初始化。返回的指针需要强制转换,失败返回nullptr。
int* p = (int*)malloc(10 * sizeof(int)); // 申请 10 个 int 的空间 if (p == NULL) { // 处理分配失败 } // 此时 p 指向的内容是未定义的 free(p); // 释放calloc:等价于malloc之后再把内存清零,适合需要初始值为 0 的场景(如数组、哈希表)。
int* p = (int*)calloc(10, sizeof(int)); // 10 个 int,全部为 0 free(p);realloc:用于扩大或缩小一块已分配的内存。它的行为需要特别注意:
- 如果原内存后面有足够空间,可能原地扩展,返回原指针;
- 如果空间不够,会分配一块新内存,把旧数据拷贝过去,释放旧内存,返回新指针;
- 如果
size为 0,行为由实现定义,可能返回nullptr或一个可释放的指针; - 如果失败,返回
nullptr,原内存保持不变,不会丢失。
int* p = (int*)malloc(10 * sizeof(int)); int* q = (int*)realloc(p, 20 * sizeof(int)); if (q == NULL) { // 扩容失败,p 仍然有效,需要自行 free(p) free(p); } else { p = q; // 扩容成功,使用新指针 } free(p);常见错误:直接写
p = realloc(p, newSize)。一旦realloc失败返回nullptr,原来的p就被覆盖,旧内存再也无法释放,造成内存泄漏。
1.4 malloc 的实现原理
malloc并不是每次都直接向操作系统要内存,而是由 C 库在用户态维护一个堆管理器,通过空闲链表等数据结构管理已申请的内存块。其核心思路大致如下:
- 内存块头部:每次分配时,在返回给用户的地址前面额外存一小段元数据(块大小、是否空闲等),这就是所谓的 chunk header。
- 空闲链表:堆管理器把空闲块按大小组织成链表(如 glibc 的 bin 机制),分配时从合适的链表中找一块够用的内存。
- 分割与合并:找到的空闲块如果比请求大,就分割成“已分配部分 + 剩余空闲部分”;释放时若相邻块空闲则合并,减少内存碎片。
- 向操作系统申请:当空闲链表里没有合适的块时,通过
brk/sbrk(小块)或mmap(大块)向内核申请新的内存区域。
// 简化示意:malloc 的核心逻辑 void* malloc(size_t size) { if (size == 0) return NULL; size = align_up(size); // 按对齐要求向上取整 Block* blk = find_free_block(size); // 在空闲链表中查找 if (blk == NULL) { blk = extend_heap(size); // 没有合适的块,向系统申请 if (blk == NULL) return NULL; } split_block(blk, size); // 分割,剩余部分放回空闲链表 blk->free = 0; return (void*)(blk + 1); // 跳过头部,返回用户可用地址 }正因为有头部元数据和空闲链表,free只需要传入用户指针,就能通过指针偏移找到块头,从而知道要释放多大的内存。这也解释了为什么malloc和free必须严格配对,且不能释放栈上或常量区的地址。
1.5 malloc 与 new 的关系
把两者放在一起对比,就能看清 C 和 C++ 在内存管理上的分工:
malloc/free只管理原始字节内存,不调用构造/析构,是 C 的接口;new/delete在malloc/free之上增加了构造与析构,是 C++ 的运算符;- 默认的
operator new内部就是调用malloc,失败时转成异常; - 因此
new分配的内存不能用free释放,malloc分配的内存也不能用delete释放。
理解了内存分区和malloc系列函数的原理,再往下看new/delete的展开过程,就会非常自然。
二、new 与 delete 的基本用法
2.1 单个对象
#include <iostream> class Widget { public: Widget() { std::cout << "Widget 构造\n"; } ~Widget() { std::cout << "Widget 析构\n"; } void run() { std::cout << "run\n"; } }; int main() { Widget* w = new Widget(); // 分配 + 构造 w->run(); delete w; // 析构 + 释放 return 0; }2.2 数组
int* arr1 = new int[3]; // 分配 3 个对象的内存,并逐个构造 int* arr2 = new int(1); // 分配 1 个对象内存,并将其初始化为1 int* arr3 = new int[10]{1,2,3,4,5}; //前五个初始化为1-5,后面数字默认为0 delete[] arr; // 逐个析构,再释放整块内存必须配对:
new配delete,new[]配delete[]。用delete释放new[]的内存是未定义行为,在多数实现上会导致只析构第一个对象,甚至堆损坏。对于错误匹配问题:
对于内置类型来说,使用new开空间使用free来释放,没有任何内存泄露
对于自定义类型,使用new开空间,使用free来释放,不会调用析构函数,有内存泄露的风险
对于内置类型,使用new[]来开空间,使用free释放不会有内存泄露(底层都是malloc和free)
对于自定义类型,使用new[]来开空间,使用delete来释放空间,当该类有析构函数时,编译器会在空间前面多开4字节的空间来存储到底要调用几次析构函数,此时使用delete来释放空间,前面的四个字节不会被释放,造成内存泄露,当该类没有析构函数时,delete也不会报错。
//new/delete class A { public: A(int a = 0,int b = 0) :_a(1) , _b(1) { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; int _b; }; void Test2() { //注意是使用花括号 A* ptr1 = new A[2]{ { 1,1 }, { 1,2 } };//使用多参数隐式类型转换 A* ptr2 = new A[2]{ A(1, 1), A(1, 2) };//使用匿名对象 }2.3 定位 new(placement new)
定位 new 不分配内存,只在一块已有内存上显式调用构造和析构函数,这是理解“分配与构造分离”的最好例子。
A* ptr3 = (A*)operator new(sizeof(A));//开空间 new(ptr3)A(1,2);//构造 ptr3->~A();//手动析构 operator delete(ptr3);//释放空间三、new 到底做了什么
编译器会把new Widget()展开成大致如下的伪代码:
// 1. 分配原始内存 void* mem = operator new(sizeof(Widget)); // 2. 在内存上构造对象 Widget* w; try { w = new (mem) Widget(); // 定位 new 调用构造函数 } catch (...) { operator delete(mem); // 构造抛异常则释放内存 throw; }关键点有三个:
- 分配与构造分离:
operator new只负责拿到一块字节内存,构造函数负责把这块内存变成对象。 - 异常安全:如果构造函数抛异常,编译器会自动调用
operator delete回收内存,不会泄漏。 - 类型安全:
new返回的是Widget*,不需要强制转换,而malloc返回void*。
new/delete 和 malloc/free 的最大区别就是对于自定义类型来说除了开空间 new/delete 会去自动调用它的构造和析构函数,内置类型无差别
四、operator new 与 operator delete
4.1 它们是什么
operator new是 C++ 标准库提供的全局函数,不是运算符重载意义上的“运算符”,它的职责只有一个:分配指定字节数的原始内存。
// 标准库中的声明(简化) void* operator new(std::size_t size); void* operator new[](std::size_t size); void operator delete(void* ptr) noexcept; void operator delete[](void* ptr) noexcept;4.2 申请失败
默认的operator new就是malloc的一层包装,malloc 申请空间失败默认返回0,new是通过抛异常实现的,符合它面向对象的特性。
4.3 与 malloc 的对比
| 对比项 | new / delete | malloc / free |
|---|---|---|
| 所属语言 | C++ 运算符 | C 库函数 |
| 是否调用构造/析构 | 是 | 否 |
| 返回类型 | 具体类型指针 | void*,需强转 |
| 大小计算 | 编译器自动计算 | 手动传字节数 |
| 失败行为 | 抛std::bad_alloc | 返回nullptr |
| 能否重载 | 可以重载operator new | 不可重载 |
| 底层实现 | 通常调用malloc | 直接向堆管理器申请 |
五、delete 做了什么
// delete w; 大致展开为: w->~Widget(); // 1. 调用析构函数 operator delete(w); // 2. 释放内存 → free(w)对于delete[],编译器需要知道数组长度才能逐个析构。因此new[]在分配时通常会多申请几个字节,把元素个数(cookie)存在返回地址之前,delete[]读取这个 cookie 后逆序析构所有元素,再调用operator delete[]释放整块内存。
这也是为什么
new[]必须配delete[]:用delete只会析构第一个元素,且传给free的指针可能不是malloc返回的原始地址,直接导致堆损坏。
六、总结
把整条链路再回顾一遍:
new T先调用operator new(sizeof(T))拿到原始内存;- 默认的
operator new内部调用malloc,失败时走new_handler并最终抛std::bad_alloc; - 拿到内存后,编译器在内存上调用构造函数,构造失败会自动回收内存;
delete先调用析构函数,再调用operator delete,最终落到free。
理解了“分配与构造分离”这一核心思想,就能明白为什么operator new可以被重载、为什么定位 new 存在、以及为什么new[]必须配delete[]。这也是从“会用 new”走向“理解 C++ 内存模型”的关键一步。