内存管理是 C/C++ 开发的核心能力之一,直接影响程序的性能、稳定性与安全性。无论是 C 语言的malloc/free,还是 C++ 的new/delete,其底层都围绕 “如何高效申请、使用与释放内存” 展开。本文将从内存分布入手,逐步剖析 C/C++ 的动态内存管理方式,对比不同方案的差异,并深入讲解new/delete的底层实现,帮你彻底掌握内存管理的核心逻辑。
一、C/C++ 程序的内存分布:搞懂 “内存去哪了”
在分析内存管理之前,首先要明确程序运行时内存的划分。C/C++ 程序的内存空间按功能可分为5 个区域,不同区域存储的数据有不同的生命周期和访问规则。
1.1 内存区域划分与功能
结合示例代码,我们先看各变量的存储位置,再总结各区域的核心作用:
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"; // 局部字符数组(存储字符串副本)
const char* pChar3 = "abcd"; // 指针(指向常量区字符串)
int* ptr1 = (int*)malloc(sizeof(int)*4); // 动态内存指针
int* ptr2 = (int*)calloc(4, sizeof(int)); // 动态内存指针
int* ptr3 = (int*)realloc(ptr2, sizeof(int)*4); // 动态内存重分配
free(ptr1);
free(ptr3);
}
各变量的存储区域如下表所示:
| 变量名 | 存储区域 | 区域功能说明 |
|---|---|---|
globalVar | 数据段(静态区) | 存储全局变量、全局静态变量、局部静态变量,程序启动时分配,结束时释放。 |
staticGlobalVar | 数据段 | 同上,静态变量无论全局 / 局部,均存于数据段。 |
staticVar | 数据段 | 同上,局部静态变量生命周期延长至整个程序运行期间。 |
localVar | 栈 | 存储非静态局部变量、函数参数、返回值,函数调用时分配栈帧,函数结束后栈帧销毁。 |
num1 | 栈 | 局部数组,栈上分配连续空间,函数结束后释放。 |
char2 | 栈 | 数组本身存于栈,"abcd" 的副本存储在栈上;*char2(数组元素)也在栈。 |
pChar3 | 栈 | 指针变量存于栈,指向代码段的 "abcd" 常量。 |
*pChar3 | 代码段(常量区) | 存储只读常量(如字符串字面量),程序运行期间不可修改,只读访问。 |
ptr1 | 栈 | 指针变量存于栈,指向堆上的动态内存。 |
*ptr1 | 堆 | 动态内存分配区域,程序运行时手动申请(malloc/new),手动释放(free/delete)。 |
ptr2/ptr3 | 栈(指针)+ 堆(指向空间) | 同上,动态内存操作均涉及 “栈上指针 + 堆上空间”。 |
1.2 各区域核心特性总结
| 内存区域 | 生长方向 | 存储内容 | 生命周期 | 访问规则 |
|---|---|---|---|---|
| 栈 | 向下增长 | 非静态局部变量、函数参数、返回值 | 函数调用期间(栈帧存在) | 自动分配 / 释放,无需手动管理 |
| 堆 | 向上增长 | 动态内存(malloc/new申请) | 手动申请至手动释放(free/delete) | 需手动管理,避免内存泄漏 |
| 数据段 | 固定 | 全局变量、静态变量 | 程序启动至程序结束 | 可读可写 |
| 代码段 | 固定 | 可执行代码、只读常量 | 程序启动至程序结束 | 只读(修改会崩溃) |
| 内存映射段 | 固定 | 动态库、共享内存 | 加载时分配,卸载时释放 | 系统管理,用户可通过接口访问 |
二、C 语言的动态内存管理:malloc/calloc/realloc/free
C 语言通过 4 个函数实现动态内存管理,核心是从堆上申请空间并手动释放。掌握它们的区别与使用场景,是理解 C++ 内存管理的基础。
2.1 函数功能与区别
| 函数 | 功能说明 | 参数要求 | 初始化情况 | 返回值 |
|---|---|---|---|---|
malloc | 从堆上申请指定字节数的连续空间 | 仅需传入 “空间大小(字节)”,如malloc(sizeof(int)*4)(申请 4 个 int 空间) | 不初始化,空间内容为随机值 | 成功返回void*,失败返回NULL |
calloc | 从堆上申请n个指定类型大小的连续空间(本质是 “计数 + 分配”) | 需传入 “元素个数n” 和 “单个元素大小”,如calloc(4, sizeof(int))(4 个 int) | 初始化所有字节为 0 | 成功返回void*,失败返回NULL |
realloc | 对已有的动态内存空间进行扩容 / 缩容,可能会移动原有数据到新空间 | 需传入 “原有空间指针” 和 “新空间大小”,如realloc(ptr, sizeof(int)*8) | 新增空间为随机值 | 成功返回新空间指针,失败返回NULL(原空间不变) |
free | 释放malloc/calloc/realloc申请的动态内存,将空间归还给堆 | 仅需传入 “动态内存指针”,如free(ptr) | 无(仅释放空间,不清理内容) | 无返回值 |
2.2 关键注意事项
-
realloc的特殊逻辑:- 若原有空间后有足够的连续空间,直接在原有空间后扩容,返回原指针;
- 若原有空间后无足够空间,会在堆上找新的连续空间,将原有数据拷贝过去,释放原空间,返回新指针;
- 若扩容失败,返回
NULL,原空间不会被释放,需避免因覆盖原指针导致内存泄漏(如ptr = realloc(ptr, ...)若失败,ptr变为NULL,原空间无法释放)。
-
free的使用禁忌:- 不能释放非动态内存(如栈上的局部变量、数据段的静态变量),否则会导致程序崩溃;
- 不能重复释放同一块动态内存,否则会触发 “双重释放” 错误;
- 释放
NULL指针是安全的(free(NULL)无任何操作),因此建议动态内存指针初始化为NULL。
-
示例:正确使用动态内存函数
void Test() {
// 1. malloc:申请4个int空间(16字节),不初始化
int* p1 = (int*)malloc(4 * sizeof(int));
if (p1 == NULL) { // 必须判空,malloc失败返回NULL
perror("malloc fail");
return;
}
// 2. calloc:申请4个int空间,初始化所有字节为0
int* p2 = (int*)calloc(4, sizeof(int));
if (p2 == NULL) {
perror("calloc fail");
free(p1); // 避免内存泄漏,先释放已申请的p1
return;
}
// 3. realloc:将p2的空间扩容到8个int(32字节)
int* p3 = (int*)realloc(p2, 8 * sizeof(int));
if (p3 == NULL) {
perror("realloc fail");
free(p2); // 扩容失败,释放原p2
free(p1);
return;
}
// 注意:realloc成功后,p2已失效(原空间可能被释放),后续用p3
p2 = NULL; // 避免野指针
// 4. 释放内存:先释放p3,再释放p1(顺序无关,只要不重复释放)
free(p3);
free(p1);
p1 = NULL; // 释放后将指针置空,避免野指针
p3 = NULL;
}
三、C++ 的动态内存管理:new/delete 操作符
C 语言的动态内存函数在 C++ 中仍可使用,但存在明显缺陷:无法自动调用自定义类型的构造函数和析构函数。因此 C++ 引入了new和delete操作符,不仅简化了内存申请,还能适配自定义类型的初始化与资源清理。
3.1 new/delete 操作内置类型
对于int、char等内置类型,new/delete与malloc/free功能类似,但语法更简洁,且失败时的处理方式不同。
3.1.1 基础语法
| 功能需求 | new/delete语法 | 对应malloc/free语法 |
|---|---|---|
| 申请单个元素空间 | int* ptr = new int; | int* ptr = (int*)malloc(sizeof(int)); |
| 申请单个元素并初始化 | int* ptr = new int(10); | int* ptr = (int*)malloc(sizeof(int)); *ptr=10; |
申请连续n个元素空间 | int* ptr = new int[10]; | int* ptr = (int*)malloc(sizeof(int)*10); |
| 释放单个元素空间 | delete ptr; | free(ptr); |
| 释放连续元素空间 | delete[] ptr; | free(ptr); |
3.1.2 关键区别
- 语法简化:
new无需手动计算空间大小(只需指定类型和个数),无需强制类型转换(malloc返回void*,需强转为目标类型)。 - 初始化支持:
new可直接初始化(如new int(10)),malloc需手动赋值。 - 失败处理:
new申请失败时抛异常(std::bad_alloc),无需判空;malloc失败返回NULL,必须判空。
示例:内置类型的 new/delete 使用
void Test() {
// 1. 申请单个int,不初始化
int* ptr1 = new int;
// 2. 申请单个int,初始化为10
int* ptr2 = new int(10);
// 3. 申请10个int的连续空间,不初始化
int* ptr3 = new int[10];
// 释放空间:单个用delete,连续用delete[],必须匹配
delete ptr1;
delete ptr2;
delete[] ptr3;
// 错误示例:不匹配使用
// delete[] ptr1; // 崩溃:单个空间用delete[]释放
// delete ptr3; // 崩溃:连续空间用delete释放
}
3.2 new/delete 操作自定义类型
这是new/delete与malloc/free的核心差异:自定义类型使用new时会调用构造函数初始化,使用delete时会调用析构函数清理资源;而malloc/free仅开辟 / 释放空间,不调用任何成员函数。
3.2.1 示例对比
class A {
public:
A(int a = 0) : _a(a) {
cout << "A():" << this << " 初始化" << endl;
}
~A() {
cout << "~A():" << this << " 资源清理" << endl;
}
private:
int _a;
};
int main() {
// 1. malloc/free操作自定义类型
A* p1 = (A*)malloc(sizeof(A)); // 仅开辟空间,不调用构造函数
free(p1); // 仅释放空间,不调用析构函数
// 输出:无任何构造/析构日志(p1指向的空间未初始化,不是完整对象)
// 2. new/delete操作自定义类型
A* p2 = new A(10); // 先开辟空间,再调用构造函数初始化
delete p2; // 先调用析构函数清理资源,再释放空间
// 输出:
// A():0x123456 初始化
// ~A():0x123456 资源清理
// 3. new[]/delete[]操作自定义类型数组
A* p3 = new A[3]; // 开辟3个A的空间,调用3次构造函数
delete[] p3; // 调用3次析构函数,再释放空间
// 输出:
// A():0x123459 初始化
// A():0x12345d 初始化
// A():0x123461 初始化
// ~A():0x123461 资源清理
// ~A():0x12345d 资源清理
// ~A():0x123459 资源清理
return 0;
}
3.2.2 核心结论
自定义类型必须使用new/delete:若用malloc,对象未调用构造函数,成员变量可能为随机值,使用时会导致逻辑错误;若用free,未调用析构函数,动态资源(如new的内存、文件句柄)会泄漏。
new[]与delete[]必须匹配:申请数组时用new[],释放时用delete[],否则析构函数调用次数错误(可能少调用或多调用,导致崩溃)。
四、深入底层:operator new 与 operator delete 函数
new和delete是用户层面的操作符,底层依赖系统提供的全局函数operator new和operator delete 完成内存申请与释放。理解这两个函数的实现,就能明白new/delete的本质。
4.1 operator new:new 的底层内存申请函数
operator new是全局函数,new操作符在申请空间时会自动调用它。其核心功能是 “通过malloc申请内存,处理申请失败的情况”。
4.1.1 源码简化分析(VS2019)
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) {
void* p;
// 循环调用malloc:若malloc失败,尝试调用用户设置的“内存不足应对措施”
while ((p = malloc(size)) == 0) {
if (_callnewh(size) == 0) { // _callnewh:用户自定义的内存不足处理函数
// 若用户未设置处理函数,抛异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
}
return p;
}
4.1.2 核心逻辑
- 依赖 malloc:
operator new本质是对malloc的封装,最终还是从堆上申请内存。 - 失败处理:
malloc失败时,不是直接返回NULL,而是:- 先调用用户设置的
_callnewh(内存不足应对函数,如释放部分无用内存); - 若应对函数也无法解决,抛
std::bad_alloc异常(这也是new无需判空的原因)。
- 先调用用户设置的
4.2 operator delete:delete 的底层内存释放函数
operator delete也是全局函数,delete操作符在释放空间时会自动调用它。其核心功能是 “通过free释放内存,处理内存块的调试信息”。
4.2.1 源码简化分析(VS2019)
void operator delete(void* pUserData) {
if (pUserData == NULL) return; // 释放NULL指针安全,直接返回
_mlock(_HEAP_LOCK); // 加锁,防止多线程同时操作堆
__TRY {
// 获取内存块的头部信息(调试用,记录内存分配类型、大小等)
_CrtMemBlockHeader* pHead = pHdr(pUserData);
// 验证内存块类型是否合法
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
// 调用_free_dbg释放内存(_free_dbg是debug版的free)
_free_dbg(pUserData, pHead->nBlockUse);
} __FINALLY {
_munlock(_HEAP_LOCK); // 解锁
}
}
// free的宏定义:debug版映射到_free_dbg,release版直接调用系统free
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
4.2.2 核心逻辑
- 依赖 free:
operator delete最终通过free释放内存,将空间归还给堆。 - 调试与线程安全:debug 版本会记录内存块的调试信息(如分配时的文件名、行号),便于定位内存泄漏;同时通过加锁 / 解锁保证多线程环境下的安全。
五、new 和 delete 的实现原理
结合operator new/operator delete,我们可以完整梳理new/delete(包括new[]/delete[])的执行流程,分为内置类型和自定义类型两种场景。
5.1 内置类型的实现原理
new/delete与malloc/free的差异仅在于 “语法简化” 和 “失败处理”,底层流程基本一致:
new T:调用operator new(sizeof(T))申请空间,返回对应类型指针;失败时抛异常。
delete ptr:调用operator delete(ptr)释放空间,底层调用free。
new T[N]:调用operator new(sizeof(T)*N)申请连续空间,返回对应类型指针。
delete[] ptr:调用operator delete(ptr)释放空间(内置类型无需析构,delete[]与delete底层释放逻辑相同,但语法上必须匹配)。
5.2 自定义类型的实现原理
自定义类型的new/delete流程增加了 “构造函数” 和 “析构函数” 的调用,这是核心差异:
5.2.1 new T 的流程(单个对象)
- 申请空间:调用
operator new(sizeof(T)),底层通过malloc申请T大小的空间。 - 初始化对象:在申请的空间上调用
T的构造函数,完成成员变量初始化和资源申请。
5.2.2 delete ptr 的流程(单个对象)
- 清理资源:调用
ptr指向对象的析构函数,释放对象内部的动态资源(如new的内存)。 - 释放空间:调用
operator delete(ptr),底层通过free释放对象的内存空间。
5.2.3 new T [N] 的流程(对象数组)
- 申请空间:调用
operator new[](sizeof(T)*N),底层调用operator new申请N*sizeof(T)的连续空间。 - 初始化数组:在连续空间上调用
N次T的构造函数,初始化每个对象。
5.2.4 delete [] ptr 的流程(对象数组)
- 清理数组资源:从后往前调用
N次T的析构函数(保证对象依赖的资源按创建逆序释放)。 - 释放空间:调用
operator delete[](ptr),底层调用operator delete释放连续空间。
六、定位 new 表达式:手动调用构造函数
在某些场景下(如内存池),我们会先申请一块原始内存(未初始化),再手动在这块内存上创建对象 —— 这就需要定位 new 表达式(placement-new),它能在已分配的内存上显式调用构造函数。
6.1 定位 new 的语法与使用场景
6.1.1 基础语法
// 格式1:无参构造
new (place_address) T;
// 格式2:带参构造(需与T的构造函数参数匹配)
new (place_address) T(initializer-list);
其中,place_address是指向已分配内存的指针(如malloc或内存池申请的内存)。
6.1.2 使用场景
内存池:内存池提前分配大块内存,避免频繁new/delete的开销;当需要创建对象时,用定位 new 在内存池的空闲块上初始化对象。
避免内存碎片:频繁申请小块内存会产生内存碎片,内存池 + 定位 new 可减少碎片。
6.1.3 示例:定位 new 的使用
class A {
public:
A(int a = 0) : _a(a) {
cout << "A():" << this << " 初始化" << endl;
}
~A() {
cout << "~A():" << this << " 资源清理" << endl;
}
private:
int _a;
};
int main() {
// 1. 用malloc申请原始内存(未调用构造函数,不是完整对象)
A* p1 = (A*)malloc(sizeof(A));
// 2. 定位new:在p1指向的内存上调用构造函数(无参)
new(p1)A;
// 3. 使用对象(此时p1指向完整对象)
// ...
// 4. 手动调用析构函数(定位new的对象需手动析构)
p1->~A();
// 5. 释放原始内存
free(p1);
// 带参构造的定位new
A* p2 = (A*)operator new(sizeof(A)); // 用operator new申请内存
new(p2)A(10); // 调用带参构造(a=10)
p2->~A(); // 手动析构
operator delete(p2); // 释放内存
return 0;
}
6.1.4 关键注意事项
必须手动析构:定位 new 创建的对象,delete不会自动调用析构函数(因为内存不是new申请的),需显式调用ptr->~T()。
内存匹配:申请内存的方式(malloc/operator new/ 内存池)需与释放方式(free/operator delete/ 内存池回收)匹配。
七、malloc/free 与 new/delete 的全面对比
通过前面的分析,我们可以从 6 个维度总结malloc/free与new/delete的差异,这也是面试高频考点:
| 对比维度 | malloc/free | new/delete |
|---|---|---|
| 本质 | 标准库函数 | C++ 操作符 |
| 语法简洁性 | 需手动计算空间大小(sizeof),返回void*需强转 | 无需计算大小(指定类型 / 个数),返回对应类型指针,无需强转 |
| 初始化支持 | 不初始化,空间内容为随机值 | 支持初始化(如new int(10)、new A(5)) |
| 失败处理 | 返回NULL,需手动判空 | 抛std::bad_alloc异常,无需判空 |
| 自定义类型适配 | 仅开辟 / 释放空间,不调用构造 / 析构函数 | 申请时调用构造函数,释放时调用析构函数 |
| 数组支持 | 需手动计算总大小(n*sizeof(T)),释放用free | 用new T[n]申请,delete[]释放,语法更清晰 |
八、总结:内存管理的核心原则
- 匹配使用:
malloc对应free,new对应delete,new[]对应delete[],不可混用,否则会导致崩溃或内存泄漏。 - 自定义类型优先用 new/delete:确保构造 / 析构函数被调用,避免资源泄漏。
- 动态内存需手动释放:C/C++ 没有垃圾回收机制,动态内存必须手动释放,建议使用智能指针(如
unique_ptr、shared_ptr)简化管理。 - 警惕野指针与内存泄漏:释放内存后将指针置空,避免野指针;动态内存申请后需确保在所有分支都能释放,避免泄漏。
内存管理是 C/C++ 开发的 “内功”,掌握底层原理不仅能写出更健壮的代码,还能快速定位内存泄漏、双重释放等疑难问题。建议结合实际项目多练习,逐步形成规范的内存管理习惯。

被折叠的 条评论
为什么被折叠?



