第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++内存管理的核心脉络画出来了。我建议你多看几遍,理清关系。

C与C++内存管理对比 C语言:手动管理 malloc/free calloc/realloc 风险:忘记释放、野指针、内存泄漏 C++:自动管理 new/delete RAII思想 智能指针 unique_ptr shared_ptr weak_ptr 进化方向 核心原则 谁分配,谁释放;RAII优先;智能指针替代裸指针

29.5 实战建议

说了这么多,到底怎么选?我给出几条实际建议:

  1. 纯C项目:老老实实用malloc/free,做好封装,每个分配点都要有对应的释放点。
  2. C++项目:优先用unique_ptr,性能好,语义清晰。需要共享时才用shared_ptr。
  3. 避免裸new/delete:除非你在实现底层容器或特殊内存池,否则用智能指针替代。
  4. RAII是信仰:任何成对出现的资源操作,都应该封装成RAII类。

我的经验:在嵌入式C++项目中,我一般禁用malloc/free,强制使用new/delete和智能指针。这样能避免很多低级错误。当然,如果你在写bootloader或者RTOS底层,那另当别论。

好了,这一章的内容就到这里。C和C++的内存管理,说白了就是:C给你自由,也给你责任;C++给你约束,也给你安全。选哪个,看你的场景和团队习惯。


公众号:蓝海资料掘金营,微信deep3321