内存管理
new and delete
用于堆上分配和释放内存,与C中malloc
和free
相似。
int* pt=new int;
int* pt1=new int[10];
delete pt;
delete[] pt1; //表明释放数组内存
MyClass* obj = new MyClass; // 调用构造函数(constructor)
delete obj; // 调用析构函数(destructor)并释放内存
- 使用 new 为对象分配内存,必须使用 delete 释放。
- 使用 new type[] 为数组分配内存,必须使用 delete[] 释放数组内存。
- delete不能用于释放非new分配的内存。
-
new (内存地址) 类型(构造参数)
用于在已分配内存上显示调用构造函数,返回类的对象
与malloc/free区别
核心区别总结
特性 | new /delete |
malloc /free |
---|---|---|
语言 | C++ 运算符 | C 标准库函数 |
构造/析构 | ✅ 自动调用构造函数/析构函数 | ❌ 不调用 |
返回类型 | 具体类型指针(如 MyClass* ) |
void* (需显式转换) |
内存来源 | 自由存储区(free store) | 堆(heap) |
大小计算 | 自动计算类型大小 | 需手动 sizeof |
失败处理 | 抛出 std::bad_alloc 异常 |
返回 NULL |
重载支持 | ✅ 可重载 operator new/delete |
❌ 不可重载 |
数组支持 | ✅ new[] /delete[] |
❌ 需手动计算数组大小 |
初始化 | ✅ 支持初始化(new int(5) ) |
❌ 只分配原始内存 |
类型安全 | ✅ 强类型 | ❌ 弱类型(易出错) |
与对象系统集成 | ✅ 完整支持面向对象 | ❌ 仅处理原始内存 |
关键差异详解:
1. 对象生命周期管理
class MyClass {
public:
MyClass() { cout << "构造\n"; }
~MyClass() { cout << "析构\n"; }
};
// new/delete
MyClass* obj1 = new MyClass; // 输出"构造"
delete obj1; // 输出"析构"
// malloc/free
MyClass* obj2 = (MyClass*)malloc(sizeof(MyClass)); // 无输出
free(obj2); // 无输出
new
:分配内存 → 调用构造函数
- delete
:调用析构函数 → 释放内存
- malloc/free
:只处理原始内存,不管理对象生命周期
2. 类型系统集成
// new - 自动类型推导
int* p1 = new int(10); // 返回 int*,初始化为10
MyClass* p2 = new MyClass; // 返回 MyClass*
// malloc - 需手动转换
int* p3 = (int*)malloc(sizeof(int)); // 返回 void* 需转换
*p3 = 10; // 未初始化(值随机)
3. 内存分配失败处理
// new - 异常机制
try {
int* p = new int[1000000000000];
} catch (const std::bad_alloc& e) {
cerr << "内存不足: " << e.what();
}
// malloc - 返回错误码
int* p = (int*)malloc(1000000000000);
if (!p) {
perror("内存分配失败");
}
4. 数组处理
// new[]/delete[]
MyClass* arr1 = new MyClass[5]; // 调用5次构造函数
delete[] arr1; // 调用5次析构函数
// malloc/free
MyClass* arr2 = (MyClass*)malloc(5 * sizeof(MyClass));
free(arr2); // 危险:未调用析构函数(资源泄漏风险)
5. 初始化能力
// new 支持直接初始化
int* p = new int(42); // 初始化为42
string* s = new string("Hello"); // 调用构造函数
// malloc 无初始化
int* p = (int*)malloc(sizeof(int));
*p = 42; // 需手动赋值
内存布局对比:
使用 new 创建对象:
┌──────────────┐
│ 对象内存 │ ← 已构造完成(含虚表指针等)
└──────────────┘
使用 malloc 创建对象:
┌──────────────┐
│ 原始内存块 │ ← 未初始化(需手动构造)
└──────────────┘
混合使用的危险:
// 错误示范1:用 free 释放 new 分配的内存
int* p = new int;
free(p); // ❌ 未调用析构函数 + 可能破坏内存管理结构
// 错误示范2:用 delete 释放 malloc 分配的内存
int* q = (int*)malloc(sizeof(int));
delete q; // ❌ 未定义行为(尝试调用不存在的析构函数)
// 正确混合使用(高级技巧)
void* mem = malloc(sizeof(MyClass));
MyClass* obj = new (mem) MyClass(); // placement new 手动构造
obj->~MyClass(); // 手动析构
free(mem); // 释放原始内存
性能差异(通常可忽略):
操作 | new/delete |
malloc/free |
说明 |
---|---|---|---|
单对象分配释放 | 稍慢 | 稍快 | new 有构造调用开销 |
批量操作 | 相当 | 相当 | 底层都调用相同内存分配器 |
自定义分配器 | ✅ 灵活 | ❌ 受限 | new 重载更强大 |
现代 C++ 中,除非在特定受限环境(如嵌入式系统),否则没有理由使用
malloc/free
。
最佳实践:
- 始终使用
new/delete
管理 C++ 对象 - 优先使用智能指针:
- 对于 POD 类型(纯数据):
- 需要底层内存控制时:
遵循 RAII 原则:资源获取即初始化,让析构函数自动处理资源释放。
栈内存管理机制
函数调用时栈的管理
首先明确在汇编中,函数调用时,栈指针 sp
会向低地址方向移动(减小),为被调用函数分配栈空间。
例如,调用前 sp = A
,调用后 sp = A-256
。此时,被调用函数的栈空间为 A-256
到 A-1
的 256 字节。
若该函数操作 A
或更高地址的内存,将覆盖调用者的栈帧数据,导致未定义行为(如栈溢出或返回地址被破坏)。
在分配的栈空间[A-256,A-1]中,函数局部变量的地址分配通常从高地址到低地址。
为什么后定义的对象先被析构?
1. 栈的「后进先出」(LIFO)特性
当对象在同一个作用域内定义时,它们的内存分配在栈上。栈的特点是后进先出:
- 构造顺序:
obj1
先构造,obj2
后构造。 - 析构顺序:
obj2
先析构,obj1
后析构。
这种反向对称的顺序确保了 依赖关系的安全性。例如,若 obj2
依赖 obj1
,则 obj1
必须在 obj2
之后析构,
否则 obj2
析构时可能访问已销毁的 obj1
。
2. 设计原则:资源释放的安全性
C++ 的析构顺序规则(后定义的对象先析构)与以下关键需求密切相关:
- 避免悬空指针/引用:若对象 A 依赖对象 B 的资源,应保证 B 的析构晚于 A。
- 资源释放顺序:某些资源(如文件句柄、锁、内存)需要按特定顺序释放。例如:
void writeToFile() {
File file("data.txt"); // 先定义:打开文件
Lock lock(file); // 后定义:锁定文件
// 操作文件...
}
// 析构顺序:lock → file
- 如果
file
先析构(关闭文件),lock
析构时可能尝试操作已关闭的文件句柄,导致未定义行为。 - 实际中,
Lock
应该依赖File
的有效性,因此File
必须后析构。
例外情况
1. 堆分配的对象(动态内存)
通过 new
创建的对象不受栈规则约束,其析构顺序由 delete
的调用顺序决定:
2. 全局/静态对象
全局对象和静态对象的构造/析构顺序由编译器决定,通常:
- 构造顺序:同一编译单元中按定义顺序构造,不同编译单元顺序不确定。
- 析构顺序:与构造顺序相反(但跨编译单元时可能不可预测)。
总结
场景 | 析构顺序规则 | 设计目的 |
---|---|---|
局部对象(栈) | 后定义的对象先析构(LIFO) | 确保依赖关系的安全性 |
堆对象 | 由 delete 顺序决定 |
用户手动控制生命周期 |
全局/静态对象 | 同一编译单元中析构顺序与构造相反 | 跨编译单元时不可预测,需谨慎使用 |
这种设计使得 C++ 的内存管理和资源释放更安全高效,尤其在 RAII(资源获取即初始化)模式中至关重要。