2. 内存分配与回收:堆与栈的区别、new/delete与malloc/free、RAII原则

内存管理这个话题,说简单也简单,说复杂能聊一整天。我做了这么多年游戏后端,见过太多因为内存问题导致的线上事故。今天咱们就把这块硬骨头啃下来。

2.1 堆与栈:两个不同的世界

先说说栈。栈是系统自动管理的,你定义一个局部变量,函数结束它就自动销毁了。速度快,效率高,但空间有限。我记得有一次在项目里,一个同事在栈上分配了一个 4MB 的数组,结果程序一跑就崩。为什么?栈空间默认也就 1-2MB,你想想看,这不炸才怪。

堆就不一样了。堆是程序员自己管理的,想分配多大就分配多大(只要系统内存够)。但代价是什么?管理成本高,分配速度慢,而且容易出内存泄漏。

对比项
分配方式 自动分配/释放 手动分配/释放
速度 极快(寄存器操作) 较慢(系统调用)
空间大小 通常 1-8MB 取决于系统内存
生命周期 函数作用域 直到手动释放
碎片问题 无碎片 容易产生碎片

核心原则:能用栈就别用堆。小对象、短生命周期的数据,栈是你的好朋友。大对象、长生命周期的数据,才考虑堆。

2.2 new/delete 与 malloc/free:C++ 和 C 的恩怨

很多新手会问:new 和 malloc 到底有什么区别?说白了,malloc 是 C 时代的产物,它只管分配内存。new 是 C++ 的,它除了分配内存,还会调用构造函数。

我见过一个坑:有人在 C++ 代码里用 malloc 分配对象,然后直接使用。结果呢?对象的构造函数没被调用,成员变量全是随机值。嗯,这种 bug 特别难查。

// 错误示范
Player* p = (Player*)malloc(sizeof(Player));
p->Init();  // 忘了调用构造函数,成员变量未初始化

// 正确做法
Player* p = new Player();  // 自动调用构造函数
delete p;                  // 自动调用析构函数

警告:千万不要混用!new 搭配 free,或者 malloc 搭配 delete,这是未定义行为。我曾经在一个遗留项目里看到这种代码,排查了整整两天才找到原因。

再补充一点:new[] 和 delete[] 要配对使用。如果你用 new[] 分配了数组,一定要用 delete[] 释放。否则只会释放第一个元素,后面的内存就泄漏了。

2.3 RAII 原则:让资源管理变简单

RAII,全称是 Resource Acquisition Is Initialization。名字听着高大上,其实核心思想很简单:资源在构造函数中获取,在析构函数中释放

为什么要这么做?你想想看,手动管理资源有多痛苦。打开文件忘了关,分配内存忘了释放,加锁忘了解锁...这些错误太常见了。RAII 就是帮你自动处理这些事。

class FileGuard {
public:
    FileGuard(const char* filename) {
        file_ = fopen(filename, "r");
        if (!file_) {
            throw std::runtime_error("打开文件失败");
        }
    }
    
    ~FileGuard() {
        if (file_) {
            fclose(file_);
            file_ = nullptr;
        }
    }
    
    // 禁止拷贝
    FileGuard(const FileGuard&) = delete;
    FileGuard& operator=(const FileGuard&) = delete;
    
private:
    FILE* file_;
};

// 使用示例
void ReadConfig() {
    FileGuard guard("config.txt");
    // 使用文件...
    // 函数结束自动关闭文件,即使发生异常也会关闭
}

个人习惯:我写代码时,只要涉及资源管理,第一反应就是封装成 RAII 类。智能指针、锁守卫、文件句柄...统统用 RAII 包装。这样代码既安全又清晰。

RAII 的好处很明显:

  • 异常安全:即使发生异常,析构函数也会被调用
  • 代码简洁:不需要到处写 try-catch-finally
  • 减少错误:不会忘记释放资源

我曾经接手过一个项目,里面到处都是裸指针和手动 new/delete。代码里充斥着各种内存泄漏和野指针。后来我带着团队花了两个月,把所有资源管理都改成了 RAII 风格。从那以后,内存相关的 bug 减少了 90% 以上。

2.4 实战建议

说了这么多,总结几条实战经验:

  1. 优先使用智能指针:std::unique_ptr 和 std::shared_ptr 是你的好帮手
  2. 避免裸 new/delete:能用 RAII 就用 RAII,实在不行才手动管理
  3. 注意生命周期:搞清楚对象的生命周期,避免悬空指针
  4. 使用内存池:对于频繁分配的小对象,用内存池代替堆分配

最后说一句:内存管理没有银弹。理解堆栈的区别,掌握 RAII 原则,再加上实战经验,你就能写出既高效又安全的代码。记住,好的代码不是写出来的,是设计出来的。