结论先行
new 的底层本质是两步操作:调用 operator new 申请内存(底层用 malloc),然后在这块内存上执行构造函数。
而 delete 恰好相反:先执行析构函数,再调用 operator delete(底层用 free)释放内存。理解这一层,就理解了 C++ 内存管理的核心设计。
一、C/C++ 程序的内存分布
程序运行时,内存被划分为几个功能不同的区域:
| 区域 | 存储内容 | 管理方式 | 增长方向 |
|---|---|---|---|
| 栈(Stack) | 非静态局部变量、函数参数、返回值 | 编译器自动分配/释放 | 向下(高地址→低地址) |
| 堆(Heap) | malloc/new 动态分配的内存 | 程序员手动分配/释放 | 向上(低地址→高地址) |
| 数据段(静态区) | 全局变量、static 变量 | 程序启动时分配,退出时释放 | --- |
| 代码段(常量区) | 可执行代码、字符串常量 | 只读 | --- |
一个经典示例
cpp
int globalVar = 1; // 数据段(已初始化)
static int staticGlobalVar = 1; // 数据段(已初始化)
void func() {
static int staticVar = 1; // 数据段(已初始化)
int localVar = 1; // 栈
char charArray[] = "abcd"; // 栈(内容从常量区拷贝过来)
char *pChar = "abcd"; // pChar 在栈,"abcd" 在常量区
int *pInt = (int*)malloc(sizeof(int)); // pInt 在栈,*pInt 在堆
}
关键区分 :charArray 和 pChar 看起来都在存字符串,但 charArray 的数组内容在栈 上(可以修改),而 pChar 指向的字符串在常量区(修改是未定义行为)。
二、C 语言动态内存管理:malloc/calloc/realloc/free
2.1 四个函数的区别
| 函数 | 功能 | 初始化 |
|---|---|---|
| malloc(size) | 申请 size 字节连续内存 | 不初始化(内容随机) |
| calloc(n, size) | 申请 n×size 字节内存 | 初始化为 0 |
| realloc(ptr, new_size) | 调整已有内存大小 | 新扩部分不初始化 |
| free(ptr) | 释放内存 | --- |
2.2 关键细节
malloc 不初始化内存,这是很多 bug 的源头。例如:
c
int *y = malloc(n * sizeof(int));
// y[i] 是随机值!
for (i = 0; i < n; i++)
y[i] += A[i][j] * x[j]; // 错!y[i] 未初始化
realloc 的陷阱 :如果原内存后面没有足够空间,realloc 会在新地址分配内存并拷贝原数据 ,返回新指针,原指针可能被自动释放 。所以永远不要写成 ptr = realloc(ptr, new_size),如果 realloc 失败会丢失原指针。
free 之后置 NULL :free(p); p = NULL; 是防御性编程的好习惯,可以避免 double-free。
三、C++ 的升级:new/delete 做了什么
3.1 为什么需要 new/delete
malloc/free 在 C++ 中有两个致命问题:
- 不调用构造函数/析构函数:malloc 只分配内存,不会初始化对象;free 只释放内存,不会清理对象资源
- 类型不安全 :malloc 返回
void*,需要手动强转,无法在编译期检查类型
3.2 new 的底层两步
cpp
A* p = new A(10);
等价于:
cpp
// 第一步:申请内存(调用 operator new)
void* raw = operator new(sizeof(A));
// 第二步:在 raw 内存上执行构造函数
A* p = new(raw) A(10); // 定位 new
而 operator new 的底层就是 malloc:
cpp
void* operator new(size_t size) {
void* p = malloc(size);
if (!p) throw std::bad_alloc(); // 失败抛异常,不返回 NULL
return p;
}
3.3 delete 的底层两步
cpp
delete p;
等价于:
cpp
p->~A(); // 第一步:执行析构函数
operator delete(p); // 第二步:释放内存(底层 free)
3.4 new\[\] / delete\[\] 的特殊处理
对于数组:
cpp
A* arr = new A[10];
编译器会在数组头部额外分配一个 8 字节的"cookie" ,记录元素个数 N。delete[] 读取这个 cookie,知道要调用几次析构函数:
cpp
// delete[] 的执行逻辑
// 1. 读取 cookie,得到 N
// 2. 对 N 个元素依次执行 ~A()
// 3. operator delete 释放整块内存(包括 cookie)
这就是为什么 new[] 必须配 delete[] :如果写成 delete arr,编译器只调用一次析构函数,但释放的是整块内存(包括 cookie),导致析构次数不匹配。
四、核心对比:malloc/free vs new/delete
| 维度 | malloc/free | new/delete |
|---|---|---|
| 本质 | 库函数 | 运算符 |
| 构造函数 | 不调用 | 调用 |
| 析构函数 | 不调用 | 调用 |
| 类型安全 | 返回 void*,需强转 | 自带类型,无需强转 |
| 大小计算 | 手动 sizeof |
编译器自动计算 |
| 初始化 | 不初始化 | 可初始化 new int(10) |
| 失败处理 | 返回 NULL | 抛出 bad_alloc 异常 |
| 重载 | 不可重载 | operator new/delete 可重载 |
一句话总结:malloc/free 只管"要内存、还内存",new/delete 还要"初始化对象、清理对象"。
五、总结
C/C++ 内存管理的核心脉络:
- 内存分布:栈(自动管理)vs 堆(手动管理),数据段/代码段(静态存储)
- C 的方案:malloc/calloc/realloc/free,只负责内存的分配与释放
- C++ 的升级 :new/delete 在 malloc/free 基础上,增加了构造函数/析构函数的自动调用
- 底层真相 :
operator new→malloc,operator delete→free,new/delete 是"malloc/free + 对象生命周期管理"
理解这一层,面试中的 "new 和 malloc 的区别" 就不再是背诵,而是真正的理解。
参考:C++ 内存分布与 new/delete 原理 ,operator new 底层实现 ,new\[\] 的 cookie 机制 。