第29章:C与C++内存管理对比:new/delete vs malloc/free、RAII、智能指针
说实话,C和C++的内存管理,就像手动挡和自动挡的区别。
C语言给你的是裸金属——malloc和free,一切你自己掌控。C++呢?它给了你更多工具,但也带来了更多规则。我做了十几年嵌入式,从纯C项目转到C++时,最大的感触就是:内存管理的方式变了,但底层逻辑没变。
这一章,我们就来掰扯清楚。
29.1 new/delete 与 malloc/free 的本质差异
很多人觉得new就是malloc的C++版本,delete就是free的C++版本。嗯,这话对了一半。
它们最核心的区别在于:new会调用构造函数,delete会调用析构函数。malloc和free只管分配和释放内存,不负责对象的生命周期。
核心差异一览
| 特性 | malloc/free | new/delete |
|---|---|---|
| 语言 | C标准库函数 | C++运算符 |
| 调用构造函数 | 否 | 是 |
| 调用析构函数 | 否 | 是 |
| 返回类型 | void* | 类型安全指针 |
| 失败行为 | 返回NULL | 抛出std::bad_alloc |
| 重载可能性 | 不可重载 | 可重载 |
| 数组分配 | 手动计算大小 | new[] / delete[] |
我在项目中遇到过这样一个坑:用malloc分配了一个C++对象,然后直接用了。结果对象里的std::string成员变量完全没初始化,程序跑着跑着就崩了。你想想看,string的构造函数没执行,内部指针是野的,不崩才怪。
// 错误示范
MyClass* obj = (MyClass*)malloc(sizeof(MyClass));
// obj 的成员变量未初始化,危险!
free(obj);
// 正确做法
MyClass* obj = new MyClass();
delete obj;
注意:千万不要混用!用malloc分配的内存,不要用delete释放;用new分配的内存,不要用free释放。行为是未定义的,我亲眼见过有人这么干,结果堆被搞坏了,排查了三天。
29.2 RAII:资源获取即初始化
RAII是C++里我最喜欢的设计思想之一。说白了就是:资源在构造函数中获取,在析构函数中释放。
为什么这玩意儿重要?因为C语言里你经常忘了free,或者提前return忘了释放。RAII把资源生命周期和对象生命周期绑定在一起,想忘都忘不了。
class FileHandler {
FILE* file;
public:
FileHandler(const char* filename) {
file = fopen(filename, "r");
if (!file) throw std::runtime_error("打开文件失败");
}
~FileHandler() {
if (file) fclose(file);
}
// 禁止拷贝...
};
void process() {
FileHandler fh("data.txt");
// 使用文件...
// 无论正常结束还是异常退出,fh析构时都会关闭文件
}
我个人习惯在C++项目里,能用RAII的地方绝不用裸资源管理。你想想看,每个资源都有人自动帮你收拾,多省心。
小技巧:RAII不仅适用于内存,还适用于文件句柄、互斥锁、数据库连接、socket等所有需要成对获取/释放的资源。
29.3 智能指针:从手动到自动
智能指针是RAII思想在内存管理上的具体实现。C++11引入了三种智能指针:unique_ptr、shared_ptr、weak_ptr。
29.3.1 unique_ptr:独占所有权
unique_ptr是独占的,不能拷贝,只能移动。说白了就是:这个对象只有一个主人。
std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
// std::unique_ptr<MyClass> ptr2 = ptr1; // 编译错误!不能拷贝
std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 可以移动
我在嵌入式项目里最喜欢用unique_ptr。为什么?因为它零开销。和裸指针相比,没有额外的引用计数开销,性能完全一样。
29.3.2 shared_ptr:共享所有权
shared_ptr使用引用计数,多个指针共享同一个对象。最后一个shared_ptr销毁时,对象才被释放。
std::shared_ptr<MyClass> sp1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> sp2 = sp1; // 引用计数+1
// 当sp1和sp2都销毁时,对象才被释放
我曾经踩过的坑:shared_ptr的循环引用问题。如果A持有B的shared_ptr,B持有A的shared_ptr,两个对象永远无法释放,内存泄漏了。解决方案是用weak_ptr打破循环。
29.3.3 weak_ptr:弱引用
weak_ptr不增加引用计数,它只是观察shared_ptr。需要使用时,通过lock()获取一个临时的shared_ptr。
std::weak_ptr<MyClass> wp = sp1;
if (auto sp = wp.lock()) {
// 对象还存在,安全使用
} else {
// 对象已被释放
}
29.4 知识体系总览
下面这张图,把C和C++内存管理的核心脉络画出来了。我建议你多看几遍,理清关系。
29.5 实战建议
说了这么多,到底怎么选?我给出几条实际建议:
- 纯C项目:老老实实用malloc/free,做好封装,每个分配点都要有对应的释放点。
- C++项目:优先用unique_ptr,性能好,语义清晰。需要共享时才用shared_ptr。
- 避免裸new/delete:除非你在实现底层容器或特殊内存池,否则用智能指针替代。
- RAII是信仰:任何成对出现的资源操作,都应该封装成RAII类。
我的经验:在嵌入式C++项目中,我一般禁用malloc/free,强制使用new/delete和智能指针。这样能避免很多低级错误。当然,如果你在写bootloader或者RTOS底层,那另当别论。
好了,这一章的内容就到这里。C和C++的内存管理,说白了就是:C给你自由,也给你责任;C++给你约束,也给你安全。选哪个,看你的场景和团队习惯。
公众号:蓝海资料掘金营,微信deep3321