3. C++内存管理核心

在嵌入式系统中,内存是最稀缺也最昂贵的资源之一。C++ 提供了比 C 更丰富的内存管理工具,但同时也带来了更多需要理解清楚的语义差异。本节围绕 new/delete、malloc/free、RAII、智能指针、自定义分配器 与 placement new 六个核心概念展开,帮助你在资源受限的 MCU 上写出既安全又高效的内存代码。

一、核心原理:两套内存分配体系

C++ 中实际存在两条并行的内存分配路径。第一条是 C 风格的 malloc/free,它只负责从堆上申请和释放原始字节块,不调用构造与析构函数;第二条是 C++ 的 new/delete,它在分配内存之后会调用对象的构造函数,释放之前会调用析构函数。理解这一点是避免内存泄漏和未定义行为的关键。

// C 风格:只分配原始内存
void* p = malloc(sizeof(Device));
// 需要手动构造
Device* d = new (p) Device(0x4001);

// C++ 风格:分配 + 构造
Device* d2 = new Device(0x4001);
delete d2;   // 析构 + 释放

// 数组形式
Device* arr = new Device[4];
delete[] arr;   // 必须用 delete[]

需要特别注意的是,new 与 malloc 不能混用:用 new 分配的内存必须用 delete 释放,用 malloc 分配的内存必须用 free 释放。混用会导致堆管理器元数据损坏,在嵌入式设备上往往表现为随机死机或 HardFault。

二、RAII:把资源生命周期绑定到对象

RAII(Resource Acquisition Is Initialization)是 C++ 最重要的资源管理思想:在构造函数中获取资源,在析构函数中释放资源。只要对象在栈上或作为成员存在,无论函数正常返回还是异常退出,析构函数都会被调用,资源自动释放。

class SpiLock {
public:
    explicit SpiLock(SpiBus& bus) : bus_(bus) { bus_.acquire(); }
    ~SpiLock() { bus_.release(); }
private:
    SpiBus& bus_;
};

void transfer() {
    SpiLock lock(spi1);   // 构造即加锁
    // ... 使用 SPI
}   // 离开作用域自动解锁

RAII 的价值在于把“记得释放”从人的责任变成编译器的责任。在中断频繁、分支众多的嵌入式代码中,这种确定性尤为重要。

三、智能指针:RAII 在堆内存上的延伸

标准库提供了三种主要智能指针,它们在嵌入式场景中的适用性差异很大:

  • std::unique_ptr:独占所有权,零额外开销,是嵌入式首选。
  • std::shared_ptr:共享所有权,带引用计数,控制块会额外占用内存并可能引入原子操作开销。
  • std::weak_ptr:配合 shared_ptr 打破循环引用,不增加引用计数。
在 Cortex-M 等无 MMU 的平台上,优先使用 unique_ptr;只有在确实需要共享生命周期时才考虑 shared_ptr,并评估其控制块带来的 RAM 与 CPU 成本。

四、自定义分配器与 placement new

嵌入式系统常常需要避免动态堆分配,或者需要把对象放到特定内存区域(如 CCM RAM、外部 SDRAM、DMA 缓冲区)。这时自定义分配器和 placement new 就派上用场。

// 静态内存池分配器
template<typename T, size_t N>
class PoolAllocator {
    alignas(T) unsigned char buf_[N * sizeof(T)];
    bool used_[N] = {};
public:
    T* allocate() {
        for (size_t i = 0; i < N; ++i)
            if (!used_[i]) { used_[i] = true; return reinterpret_cast<T*>(&buf_[i * sizeof(T)]); }
        return nullptr;
    }
    void deallocate(T* p) {
        size_t i = (reinterpret_cast<unsigned char*>(p) - buf_) / sizeof(T);
        used_[i] = false;
    }
};

// placement new:在指定地址构造对象
alignas(Device) unsigned char storage[sizeof(Device)];
Device* dev = new (storage) Device(0x4001);
dev->~Device();   // 显式析构

placement new 不分配内存,只在给定地址上调用构造函数,因此必须手动调用析构函数。它常用于内存池、共享内存、DMA 描述符等场景。

五、对比与选型清单

机制是否调用构造/析构典型用途嵌入式注意点
malloc/free否原始缓冲区、C 接口不初始化,需手动构造
new/delete是普通对象可能抛异常,需评估堆碎片
RAII是锁、文件、外设句柄析构顺序要明确
unique_ptr是独占堆对象零开销,首选
shared_ptr是共享生命周期控制块开销,慎用
自定义分配器视实现内存池、固定块避免碎片,确定性高
placement new仅构造指定地址构造必须显式析构

六、案例与误区复盘

某工业网关项目在 STM32F4 上运行,初期使用 new/delete 频繁创建和销毁通信会话对象。运行数小时后出现 HardFault。排查发现堆碎片导致 new 返回 nullptr,而代码未检查返回值便直接解引用。修复方案是改用固定大小内存池 + placement new,彻底消除碎片,并把分配失败路径显式处理。

常见误区:
1. new[] 用 delete 释放,或反之;
2. 用 free 释放 new 的对象;
3. placement new 构造后忘记显式调用析构;
4. 在中断服务程序中调用 malloc/new,破坏可重入性;
5. 认为 shared_ptr 没有开销而滥用。

掌握这六种机制的关键,是理解“谁分配、谁构造、谁析构、谁释放”这条完整责任链。在嵌入式环境中,优先选择确定性高、碎片少、开销可控的方案,才能让内存管理真正服务于系统稳定性。

C++ 内存管理核心机制关系图 C 风格分配 malloc / free 仅分配原始字节 不调用构造/析构 需手动初始化 C++ 风格分配 new / delete 分配 + 构造 析构 + 释放 类型安全 RAII 核心思想 构造获取 / 析构释放 生命周期绑定对象 异常安全 确定性释放 智能指针 unique_ptr(零开销) shared_ptr(引用计数) weak_ptr(打破循环) 自定义分配器 固定块内存池 避免堆碎片 确定性分配时间 placement new 指定地址构造 不分配内存 必须显式析构 责任链:谁分配 → 谁构造 → 谁析构 → 谁释放