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 实战建议
说了这么多,总结几条实战经验:
- 优先使用智能指针:std::unique_ptr 和 std::shared_ptr 是你的好帮手
- 避免裸 new/delete:能用 RAII 就用 RAII,实在不行才手动管理
- 注意生命周期:搞清楚对象的生命周期,避免悬空指针
- 使用内存池:对于频繁分配的小对象,用内存池代替堆分配
最后说一句:内存管理没有银弹。理解堆栈的区别,掌握 RAII 原则,再加上实战经验,你就能写出既高效又安全的代码。记住,好的代码不是写出来的,是设计出来的。