Skip to content

内存管理

new and delete

用于上分配和释放内存,与C中mallocfree相似。

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 (内存地址) 类型(构造参数) 用于在已分配内存上显示调用构造函数,返回类的对象

     Person* person = new (buffer) Person("张三", 30);
    

与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


最佳实践:

  1. 始终使用 new/delete 管理 C++ 对象
  2. 优先使用智能指针:
    #include <memory>
    auto ptr = std::make_unique<MyClass>(); // C++14+
    
  3. 对于 POD 类型(纯数据):
    // POD 结构示例
    struct Point { int x, y; }; 
    
    // 仍推荐使用 new(保证一致性)
    Point* p = new Point{10, 20};
    delete p;
    
  4. 需要底层内存控制时:
    // 使用 vector 替代手动数组
    std::vector<int> arr(100); // 自动管理
    

遵循 RAII 原则:资源获取即初始化,让析构函数自动处理资源释放。

栈内存管理机制

函数调用时栈的管理

首先明确在汇编中,函数调用时,栈指针 sp 会向低地址方向移动(减小),为被调用函数分配栈空间。
例如,调用前 sp = A,调用后 sp = A-256。此时,被调用函数的栈空间为 A-256A-1 的 256 字节。 若该函数操作 A 或更高地址的内存,将覆盖调用者的栈帧数据,导致未定义行为(如栈溢出或返回地址被破坏)。 在分配的栈空间[A-256,A-1]中,函数局部变量的地址分配通常从高地址到低地址。

为什么后定义的对象先被析构?

1. 栈的「后进先出」(LIFO)特性

当对象在同一个作用域内定义时,它们的内存分配在栈上。栈的特点是后进先出:

void example() {
    Object obj1;  // 先定义
    Object obj2;  // 后定义
} 
// 析构顺序:obj2 → obj1
  • 构造顺序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(资源获取即初始化)模式中至关重要。

Comments