2. C/C++内存管理机制

本章聚焦 C/C++ 程序内存管理的三大支柱:C 风格的手动内存管理(malloc/free)、C++ 的内置操作符(new/delete)以及现代 C++ 推荐的智能指针(Smart Pointer)。理解三者的底层原理、适用场景及常见陷阱,是编写无内存泄漏代码的前提。本节将通过实现流程对比、API 解析与实战原则,建立一套清晰的内存管理决策框架。

2.1 内存分配与释放的二维分类

维度C 风格 (malloc/free)C++ 风格 (new/delete)C++ 现代风格 (智能指针)
分配源stdlib 堆自由存储(通常也是堆)自由存储 + 栈(控制块)
失败行为返回 NULL抛出 std::bad_alloc抛出 std::bad_alloc(make_shared/make_unique)
构造/析构调用不调用调用构造函数与析构函数自动调用(RAII)
手动释放必须 free()必须 delete无需手动释放,作用域结束自动调用删除器
异常安全较差,需手动处理部分安全,配合 RAII 类优秀,天然异常安全

2.2 malloc/free 实现原则与核心流程

malloc 不执行构造函数,只分配原始字节;free 不执行析构函数。对非 POD 类型直接使用 malloc 分配内存是未定义行为的主要来源。

2.2.1 标准使用模式

// 正确:使用 malloc + 显式计算大小(非 POD 禁止)
int* p = (int*)malloc(10 * sizeof(int));
if (p != NULL) {
    // 使用内存
    p[0] = 42;
    free(p);       // 防止内存泄漏
}
// 错误:忘记检查 NULL 或忘记 free
char* buf = (char*)malloc(256);
// ... 若中间 return 或无 free,泄漏

2.2.2 常见错误谱

  • 忘记 free:作用域结束后指针丢失,无法回收。
  • 重复 free:double free 导致 heap corruption。
  • free 非 malloc 指针:传递栈地址或已释放地址。
  • 越界写入:写入超出申请大小破坏堆元数据,后续 free 崩溃。
重要避坑:不要用 malloc/free 管理含有虚函数或 std::string 等非平凡类型的对象。构造与析构缺失导致成员未初始化、内存泄漏或资源泄漏。

2.3 new/delete 重载与组合

C++ 中的 new 表达式实际由两步组成:调用 operator new(分配内存) + 调用构造函数。理解这一过程有助于定位自定义内存池调试中的异常。

2.3.1 分解 new 与 delete

// 内置原理:new = operator new(size_t) + 构造
void* raw = operator new(sizeof(MyClass));   // 1. 分配
MyClass* obj = new(raw) MyClass(args);       // 2. placement new 构造
// delete = 析构 + operator delete(void*)
obj->~MyClass();                              // 1. 析构
operator delete(obj);                         // 2. 释放

2.3.2 数组 new/delete 的陷阱

使用 new[] 必须搭配 delete[],否则只有第一个元素被析构,后续元素泄漏。

// 错误:delete 误用于数组
MyClass* arr = new MyClass[10];
delete arr;   // 只析构 arr[0],arr[1..9] 泄漏 + 堆元数据损坏
// 正确
delete[] arr;
调试技巧:在 Linux 上使用 valgrind --tool=memcheck 可立即检出 new[]/delete 不匹配。Windows 上的 CRT(C 运行时库)调试版本会在退出前报告泄漏。

2.4 智能指针使用原则与生命周期管理

现代 C++ 推荐使用智能指针实现 RAII(资源获取即初始化),从根本上减少手动 delete。三者分别解决独占、共享与弱引用的场景。

2.4.1 三种智能指针的决策树

  • std::unique_ptr:独占所有权,移动语义,零开销抽象。最适合作用域内管理或作为类的唯一所有权成员。
  • std::shared_ptr:共享所有权,引用计数,有额外的控制块开销。适合多个对象同时持有同一资源。
  • std::weak_ptr:配合 shared_ptr 使用,解决循环引用,不增加引用计数。

2.4.2 核心使用模式

#include <memory>

// 优先使用 make_unique / make_shared
auto ptr = std::make_unique<std::string>(10, 'c');   // C++14
auto sptr = std::make_shared<std::vector<int>>(100);

// 禁止:裸 new + 原始指针赋值到智能指针
// std::shared_ptr<int> bad(new int(5));   // 可工作,但不异常安全

// 正确:转移所有权
std::unique_ptr<int> source = std::make_unique<int>(42);
std::unique_ptr<int> dest = std::move(source);   // source 置空

// 循环引用演示与 weak_ptr 解法
struct B;   // 前向声明
struct A {
    std::shared_ptr<B> b_ptr;
    ~A() { /* ... */ }
};
struct B {
    std::weak_ptr<A> a_wptr;   // 改为 weak_ptr,不增加引用计数
    ~B() { /* ... */ }
};

2.4.3 性能与稳定性影响

特性unique_ptrshared_ptrweak_ptr
内存开销与裸指针相同控制块(约 2 字)控制块 + 弱计数
线程安全同指针操作引用计数原子操作lock() 临时提升时原子操作
适用场景局部资源、数组、Pimpl缓存、共享数据、观察者打破循环引用
性能优化原则:对高频率分配的小对象,std::shared_ptr 的原子增量可能成为瓶颈。可考虑使用 custom deleter 或改用 unique_ptr 配合 reference wrapper/observer_ptr 替代共享所有权。
智能指针生命周期与所有权转移决策流 自动存储期(栈) ➡ 智能指针对象 unique_ptr 独占所有权 shared_ptr 共享所有权 + 引用计数 weak_ptr 弱引用,不增加计数 移动 拷贝 动态存储期(堆) ➡ 托管对象 托管 Object 共享 Object 引用计数: 2 弱引 Object 引用计数: 1, weak: 1 核心规则:栈上智能指针析构 ➡ 堆上对象自动释放(delete 或弱引用失效) unique_ptr 出作用域立刻释放;shared_ptr 引用计数归零释放;weak_ptr 不触发释放

2.5 调试检测:Valgrind 与 Address Sanitizer

对 malloc/free 与 new/delete 的泄漏检测,推荐两套工具链。

2.5.1 编译选项(GCC/Clang)

# 启用 Address Sanitizer (ASan)
cmake -B build -DCMAKE_CXX_FLAGS="-fsanitize=address -g -O1"
cmake --build build

# 运行可执行文件,泄漏或越界时直接打印详细堆栈
./build/sample

2.5.2 Valgrind 调用示例

# 无泄漏输出样例
valgrind --tool=memcheck --leak-check=full ./my_program
# 输出摘要: "All heap blocks were freed -- no leaks are possible"

# 泄漏输出样例
# "LEAK SUMMARY:
#    definitely lost: 256 bytes in 1 blocks"
部署建议:在 CI/CD 流程中将 ASan 或 Valgrind 作为性能测试前的必过关卡,防止泄漏代码合并到主分支。

2.6 本章常见错误一览

  1. 混用 malloc/free 与 new/delete:用 free 释放 new 分配的内存,导致析构函数不执行和堆元数据损坏。
  2. 使用 auto_ptr:C++17 已废弃,拷贝语义导致控制权隐式转移,容易悬空。应升级为 unique_ptr。
  3. shared_ptr 循环引用:父子互相持有 shared_ptr,导致引用计数永不归零。解决方案:将一方改为 weak_ptr。
  4. 忘记 reset 导致 shared_ptr 泄漏:全局或静态 shared_ptr 未及时 reset,进程退出时才释放,对长时间运行服务就是永久泄漏。
  5. 在容器中存储裸指针而非智能指针:容器析构时只释放容器本身,内部指针所指对象全部泄漏。

2.7 核心实践原则

  • 默认使用 std::make_unique / std::make_shared,禁止直接 new 赋值给智能指针。
  • 只在必须重写分配器或实现内存池时,才直接使用 placement new 和 operator delete。
  • 将智能指针作为类成员时,明确所有权语义:unique_ptr 独占,shared_ptr 共享,weak_ptr 非拥有观察。
  • 性能敏感路径:在帧循环或高频调用中优先使用 unique_ptr 或裸指针传参(不转移所有权)。shared_ptr 的控制块开销和原子操作可能引入数十乃至上百纳秒延迟。
  • 调试阶段开启 ASan 或 Valgrind,并在单元测试中覆盖内存泄漏检测。