在嵌入式系统上使用c++有哪些需要注意的? (内存与性能限制)

嵌入式C++需避免STL动态分配、虚函数和异常等隐式开销,改用std::array、CRTP、静态内存池,并严格控制栈空间与编译选项。

避免 STL 容器动态内存分配

嵌入式系统通常没有完整堆管理或禁用 malloc/free,而标准 STL 容器(如 std::vectorstd::string)默认在堆上分配内存,运行时可能崩溃或不可预测。

  • 改用固定大小的栈数组或预分配缓冲区,例如用 std::array 替代 std::vector
  • 若必须用动态容器,选用无堆依赖的替代库

    (如 etlfolly::small_vector 的裁剪版),并确保其配置为使用静态内存池
  • 禁用异常和 RTTI 后,部分 STL 实现仍会隐式调用 operator new——需检查编译器链接的 libstdc++/libc++ 是否启用了 -fno-use-cxa-atexit 和自定义 new 操作符

慎用虚函数与运行时多态

虚函数表(vtable)增加 ROM 占用,虚调用引入间接跳转开销,在 Cortex-M0/M3 等无分支预测的小核上延迟明显;且每个含虚函数的类实例额外携带 4 字节 vptr。

  • 优先用模板 + CRTP(Curiously Recurring Template Pattern)实现编译期多态
  • 若必须运行时分发,考虑函数指针表或状态机枚举,而非继承+虚函数
  • 确认编译器是否内联了简单虚函数调用(g++ -O2 可能优化掉单实现类的虚调用,但不可依赖)

控制编译器生成代码体积与延迟

默认 C++ 编译选项常为通用场景优化,嵌入式需主动约束:避免模板爆炸、抑制冗余符号、关闭非必要 ABI 特性。

  • 添加 -fno-exceptions -fno-rtti -fno-threadsafe-statics,防止隐式插入异常处理桩和 guard 变量逻辑
  • 对时间敏感函数加 __attribute__((optimize("O3,fast-math,no-tree-vectorize")))(注意 fast-math 可能破坏 IEEE 浮点语义)
  • arm-none-eabi-g++ -ffunction-sections -fdata-sections 配合链接脚本 --gc-sections 删除未引用代码/数据
  • 检查 sizeofalignof —— 某些 STL 类型(如 std::function)在不同工具链下尺寸差异极大,可能意外撑爆栈

栈空间必须显式估算并监控

嵌入式通常只配几 KB 栈(如 2KB),而 C++ 默认栈行为比 C 更“激进”:临时对象、隐式拷贝、异常栈展开都会快速耗尽空间,且多数 MCU 无栈溢出检测。

  • 禁止递归调用,所有函数调用深度应静态可分析(可用 arm-none-eabi-gcc -fstack-usage 生成 .su 文件)
  • 避免大尺寸局部对象:std::array 直接压栈,等价于裸数组但更易被忽略
  • 重载全局 operator newoperator delete 为 panic-on-fail,而非返回 nullptr;同时禁用 placement new 以外的所有 new 形式
  • 启动时用 MPU 或软件标记栈末尾(如填魔数),运行中定期校验——很多硬故障实际是静默栈溢出覆盖了相邻变量
extern "C" void* operator new(size_t size) {
    while(1); // 或触发 HardFault
}
extern "C" void operator delete(void*) noexcept {}

C++ 在资源受限环境不是不能用,而是每个语法特性都要问一句:它悄悄申请了什么内存?增加了多少指令周期?有没有替代的、更透明的写法?最危险的从来不是写不出功能,而是没意识到某行看似干净的 std::string s = "hello" 正在往只剩 64 字节的栈里塞 24 字节对象加堆分配。