内存管理是C/C++开发的基石,也是面试与工程实践中的高频考点。本文将从内存分布、动态内存管理方式、底层实现原理等维度,系统梳理malloc/free与new/delete的核心知识与差异。
一、C/C++ 内存分布
1. 内存区域划分
C/C++程序的虚拟地址空间通常分为以下几个核心区域:
|----------|--------------------|--------------------|
| 区域 | 存储内容 | 特点 |
| 内核空间 | 操作系统内核代码和数据 | 用户代码无法直接访问 |
| 栈(Stack) | 非静态局部变量、函数参数、返回值等 | 向下增长,由编译器自动分配释放 |
| 内存映射段 | 动态库、共享内存、文件映射等 | 高效的I/O映射方式,用于进程间通信 |
| 堆(Heap) | 动态内存分配(malloc/new) | 向上增长,由用户手动管理 |
| 数据段 | 全局变量、静态变量(static) | 程序运行期间一直存在 |
| 代码段 | 可执行代码、字符串常量 | 只读,防止程序意外修改指令 |
- 代码示例与变量位置分析
cpp
int globalVar = 1; // 数据段(全局区)
static int staticGlobalVar = 1; // 数据段(静态全局区)
void Test()
{
static int staticVar = 1; // 数据段(静态局部区)
int localVar = 1; // 栈
int num1[10] = {1, 2, 3, 4}; // 栈(数组名num1在栈,元素也在栈)
char char2[] = "abcd"; // 栈(数组char2在栈,字符串拷贝到栈上)
const char* pchar3 = "abcd"; // pchar3在栈;"abcd"在代码段(常量区)
int* ptr1 = (int*)malloc(sizeof(int) * 4); // ptr1在栈;指向的空间在堆
int* ptr2 = (int*)calloc(4, sizeof(int)); // ptr2在栈;指向的空间在堆
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); // ptr3在栈;指向的空间在堆
free(ptr1);
free(ptr3);
}
详细解读
- 全局变量与静态变量
◦ globalVar:全局变量,存储在数据段,程序运行期间一直存在。
◦ staticGlobalVar:静态全局变量,存储在数据段,作用域限制在当前文件。
◦ staticVar:静态局部变量,存储在数据段,只在第一次进入Test函数时初始化,生命周期贯穿整个程序。
- 局部变量与数组
◦ localVar:非静态局部变量,存储在栈上,函数执行完毕后自动释放。
◦ num1:数组名是一个指针常量,存储在栈上,它指向的10个int元素也连续存储在栈上。
◦ char2[]:数组名char2在栈上,字符串"abcd"会被拷贝到栈上的数组空间中,因此*char2也在栈上。
◦ pchar3:指针变量本身在栈上,但它指向的字符串常量"abcd"存储在代码段(只读常量区)。
- 动态内存分配
◦ ptr1, ptr2, ptr3:这三个指针变量都存储在栈上。
◦ 它们通过malloc/calloc/realloc函数在堆上申请了内存空间,因此*ptr1, *ptr2, *ptr3指向的内存都在堆上。
◦ 注意:realloc调整了ptr2指向的内存块大小,成功后ptr2指向的内存已被ptr3接管,因此代码中只需要free(p3)。
二、C语言动态内存管理方式:malloc/calloc/realloc/free
1. 函数区别
|----------------------------------|--------------------|--------------|---------------------|
| 函数 | 功能 | 初始化 | 注意事项 |
| malloc(size_t size) | 申请指定字节数的内存 | 不初始化,内容为随机值 | 需手动计算大小,返回void* |
| calloc(size_t num, size_t size) | 申请num个大小为size的连续内存 | 自动将所有字节初始化为0 | 等价于malloc + memset |
| realloc(void* ptr, size_t size) | 调整已分配内存块的大小 | 新扩展的内存不初始化 | 若ptr为NULL,等价于malloc |
| free(void* ptr) | 释放已分配的内存 | - | 必须成对使用,避免内存泄漏 |
2. 代码示例
cpp
void Test ()
{
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里不需要free(p2),因为realloc成功后,p2指向的内存已被管理,p3是新的指针
free(p3);
}
3. 面试题
- malloc/calloc/realloc的区别?
malloc:只分配,不初始化。
calloc:分配并初始化全0。
realloc:调整已有内存大小,可能会移动内存块。
- malloc的实现原理?(glibc中malloc实现原理)
底层通过brk或mmap系统调用向操作系统申请内存。
维护内存块链表(bins),优先在空闲链表中查找合适的块,避免频繁系统调用。
采用伙伴系统或类似算法管理内存碎片。
三、C++内存管理方式:new/delete
1. 操作内置类型
cpp
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
delete ptr4;
delete ptr5;
delete[] ptr6; // 注意:释放数组必须用delete[]
}
注意:
• 申请/释放单个元素:new / delete
• 申请/释放数组:new[] / delete[],必须配对使用。
2. 操作自定义类型
cpp
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A)); // 只开空间,不调用构造
A* p2 = new A(1); // 开空间 + 调用构造函数
free(p1); // 只释放空间,不调用析构
delete p2; // 调用析构函数 + 释放空间
// 内置类型是几乎是一样的
int* p3 = (int*)malloc(sizeof(int));
int* p4 = new int;
free(p3);
delete p4;
// 数组
A* p5 = (A*)malloc(sizeof(A)*10);
A* p6 = new A[10]; // 调用10次构造函数
free(p5);
delete[] p6; // 调用10次析构函数
return 0;
}
核心结论
- 自定义类型
◦ malloc/free:只负责堆空间的分配与释放,不会调用构造函数和析构函数。如果对象内部持有资源(如文件句柄、动态内存),使用 free 会导致资源泄漏。
◦ new/delete:除了分配/释放空间,还会自动调用构造函数初始化对象和调用析构函数清理资源,保证了对象生命周期的完整性。
- 内置类型(如 int)
◦ 由于没有构造和析构函数,malloc/free 和 new/delete 的行为几乎一致,都只是单纯的内存管理。
- 数组处理
◦ new T[N]:会连续调用 N 次构造函数。
◦ delete[]:会连续调用 N 次析构函数。
◦ 必须配对使用:new[] 必须与 delete[] 配对,new 必须与 delete 配对,否则会导致未定义行为(如内存泄漏或程序崩溃)。
注意:
• new:先调用operator new分配空间,再调用构造函数。
• delete:先调用析构函数,再调用operator delete释放空间。
四、operator new与operator delete函数
1. 基本概念
• new和delete是用户操作符,其底层是通过调用全局函数operator new和operator delete来实现的。
• operator new和operator delete是可以被重载的全局函数。
2. operator new的实现(简化版)
cpp
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
void* p;
while ((p = malloc(size)) == 0)
{
if (_callnewh(size) == 0)
{
// 如果申请内存失败,抛出bad_alloc异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
}
return (p);
}
原理:
• 本质上是通过malloc来申请空间。
• 如果malloc成功,直接返回。
• 如果失败,尝试执行用户提供的_callnewh空间不足应对措施。
• 如果用户没有提供措施或措施失败,则抛出bad_alloc异常。
最核心结论(面试必问)
- new = operator new + 构造函数
operator new:只开空间 ; 之后编译器自动调用构造函数
-
operator new 底层就是 malloc
-
内存不足时: 先重试 ; 再看有没有 new_handler ; 都不行 → 抛 bad_alloc
-
malloc 失败返回 NULL ;operator new 失败抛异常
总结:operator new 就是"带异常、带重试的 malloc 封装"。
3. operator delete的实现(简化版)
cpp
void operator delete(void *pUserData)
{
if (pUserData == NULL)
return;
// ... 内部处理 ...
_free_dbg(pUserData, _NORMAL_BLOCK); // 最终调用free释放空间
}
原理:
• 本质上是通过free来释放空间。
最核心结论(面试必背)
-
operator delete 底层 = 最终调用 free
-
Debug 模式多了内存检测,但本质不变
-
delete 真正做两件事:调用析构函数 ;调用 operator delete → 释放内存
-
operator delete 只负责释放空间,不负责析构!
总结:operator delete 就是带安全检查、最终包了一层 free 的释放函数。
4. new[] 搭配 delete[] 的原因
new[] 会多开4个字节,存"对象个数",delete[] 要靠这4个字节才知道要调用多少次析构函数。
如果混用,会内存泄漏 / 崩溃 / 未定义行为。
- 当你写:A* p = new A[5];
编译器实际做了这 3 件事:
-
计算总大小:5个对象 + 4个字节(存个数 5)
-
多开 4 字节:真实开辟大小 = 5 * sizeof(A) + 4;这 4 个字节用来存 5,告诉系统有多少个对象。
-
调用 5 次构造函数
-
当你用正确的 delete[] :delete[] p;
编译器做 2 件事:
-
往前读4个字节,拿到对象个数 5
-
对 5 个对象依次调用析构函数
-
把整块内存(包括那4个字节)一起释放
✅ 完全正确
- 如果你错误用 delete delete p; // 错!
编译器只会做:
-
只调用 1 次析构函数:剩下4个对象没析构 → 内存泄漏
-
只释放一个对象大小:系统不知道有5个 → 堆损坏、崩溃
❌ 未定义行为(崩溃/泄漏/报错)
最关键结论(面试必背)
-
new[] = 开空间 + 存对象个数 + 多次构造
-
delete[] = 读个数 + 多次析构 + 释放整块空间
-
delete 不知道有多少个对象,只会析构1个
-
内置类型(int/char...)一般不会崩,因为没有析构函数,但依然是未定义行为!
最简单记忆口诀
谁开的空间,谁来释放: new → delete ; new[] → delete[]
五、new和delete的实现原理
1. 内置类型
• new:调用operator new分配空间,若失败则抛异常。
• delete:调用operator delete释放空间。
• new[]:调用operator new[]分配空间。
• delete[]:调用operator delete[]释放空间。
2. 自定义类型
• new的原理:
-
调用operator new函数申请空间。
-
在申请的空间上执行构造函数,完成对象的构造。
• delete的原理:
-
在空间上执行析构函数,完成对象中资源的清理工作。
-
调用operator delete函数释放对象的空间。
• new T[N]的原理:
-
调用operator new[]函数,在operator new[]中实际调用operator new完成N个对象空间的申请。
-
在申请的空间上执行N次构造函数。
• delete[]的原理:
-
在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
-
调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。
六、定位new表达式(placement-new)
1. 核心概念
定位new表达式是在已分配的原始内存空间中,显式调用构造函数来初始化一个对象。
2. 使用格式
cpp
new (place_address) type;
// 或
new (place_address) type(initializer-list);
• place_address:必须是一个指针,指向已分配的内存。
• initializer-list:类型的初始化列表。
3. 使用场景
• 主要配合内存池使用。
• 因为内存池分配出的内存没有初始化,如果是自定义类型的对象,需要用定位new来显式调用构造函数进行初始化。
4. 代码示例
cpp
class A
{
public:
// 构造函数:打印地址,初始化 _a ;析构函数:打印地址 ;没有写拷贝构造、赋值重载,默认即可
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
// malloc:只开空间,不调用构造;此时 p1 指向一块内存,但还不是合法对象
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 定位new,在p1指向的空间上构造一个A对象,在已经有的内存上,调用构造函数
p1->~A(); // 显式调用析构函数,只做清理工作,不释放内存
free(p1); // 释放空间,真正释放 malloc 出来的空间
A* p2 = (A*)operator new(sizeof(A));
// operator new 底层就是 malloc;作用:只开空间,不构造
new(p2)A(10); // 定位new,带参构造
p2->~A(); // 显示析构
operator delete(p2); // 底层就是 free,释放空间
return 0;
}
运行输出(大致样子)
cpp
A():0xXXXXXX
~A():0xXXXXXX
A():0xYYYYYY
~A():0xYYYYYY
每个对象:构造一次,析构一次,完全匹配。
核心总结
-
malloc / operator new :只分配内存,不调用构造
-
free / operator delete :只释放内存,不调用析构
-
定位 new:new(地址) 类名(参数) :在已有地址上调用构造函数
-
显式析构:p->~类名() :只调用析构,不释放内存
-
析构函数是对象的方法,只负责对象内部。内存是 malloc / operator new 分配的,必须由 free / operator delete 归还。
C++ 设计就是:构造 / 析构 管对象生命周期 ;new / delete、malloc / free 管内存分配释放。
构造:造对象 析构:毁对象(不清内存)
malloc / operator new:拿内存 free / operator delete:还内存
平常写的 new / delete 到底干了啥?
A* p = new A;
等价于:1. operator new 开空间 2. new(p) A 定位new构造
delete p;
等价于:1. p->~A() 显式析构 2. operator delete(p) 释放空间
七、malloc/free和new/delete的区别
1. 本质
malloc/free:库函数
new/delete:C++ 运算符
2. 初始化
malloc:只开空间,不初始化
new:可以直接初始化(如 new int(10))
3. 空间大小
malloc:必须手动计算字节数 ;例:malloc(sizeof(int)*4)
new:只需写类型,自动计算大小 ;例:new int[4]
4. 返回值
malloc:返回 void*,必须强转
new:返回对应类型指针,不用强转
5. 失败处理
malloc 失败:返回 NULL
new 失败:抛出 bad_alloc 异常
6. 自定义类型
malloc/free:只开空间/释放空间,不调用构造、析构
new/delete:
new:开空间 + 调用构造函数
delete:调用析构函数 + 释放空间
malloc/free 只负责内存;new/delete 不仅负责内存,还会自动调用构造与析构函数。
|--------|-------------|---------------------|
| 特性 | malloc/free | new/delete |
| 本质 | 库函数 | C++运算符 |
| 初始化 | 不初始化 | 可初始化(如 new int(10)) |
| 空间大小 | 手动计算字节数 | 自动计算,数组用 [] |
| 返回值 | void*,必须强转 | 对应类型指针,无需强转 |
| 内存申请失败 | 返回 NULL | 抛出 bad_alloc 异常 |
| 自定义类型 | 只开辟/释放空间 | 自动调用构造函数和析构函数 |
掌握C/C++内存管理,不仅是写出高效代码的基础,更是理解程序运行机制的关键。从内存分布到new/delete的底层实现,每一个细节都值得我们深入探究,在实践中不断积累,稳步进阶。