摘要:在单片机(MCU)开发领域,C 语言长久以来占据着统治地位。然而,随着 ARM Cortex-M 系列性能的提升和编译器技术的进步,Modern C++ (C++11/14/17) 正以一种“零成本抽象”的姿态入侵底层。本文将从汇编视角深度剖析 C 与 C++ 的利弊,揭示 C++ 如何通过模板元编程和 RAII 机制,在不损失 1 字节性能的前提下,实现对 C 语言的降维打击。


一、 C 语言:由于“透明”所以“统治”

为什么 Linus Torvalds 痛骂 C++?为什么 FreeRTOS、Linux 内核至今仍坚持用 C? 答案只有一个:ABI (Application Binary Interface) 的稳定性与底层行为的透明性。

1. 优势:所见即所得 (WYSIWYG)

在 C 语言中,你写下的每一行代码,几乎都能在大脑中直接映射成汇编指令。

  • a = b + c -> ADD R0, R1, R2

  • struct -> 内存中连续的字节块。

  • 函数调用 -> BL 指令,参数压栈或存寄存器。

这种“透明性”让嵌入式工程师极具安全感。你不需要担心编译器会在背后偷偷生成 1KB 的代码去处理异常,也不用担心隐式的内存分配。

2. 劣势:抽象能力的贫瘠

为了实现通过“对象”来管理硬件,C 语言不得不使用大量的**“模拟面向对象”**技巧:

// C 语言模拟的 "类"
typedef struct {
    uint32_t (*Read)(void);
    void (*Write)(uint32_t);
} UART_Driver;

// 运行时开销:间接函数调用 (Indirect Call)
// 汇编:LDR R3, [R0, #offset]; BLX R3
driver->Write(0x55);

痛点

  • 性能损耗:函数指针会导致 CPU 流水线断流,且编译器无法内联 (Inline) 优化。

  • 类型不安全void* 满天飞,强转全靠程序员的自觉。

  • 宏地狱:为了复用代码,只能写出令人作呕的多行宏定义。


二、 C++ 的误解:它是“洪水猛兽”吗?

很多嵌入式工程师拒绝 C++ 的理由通常是:“慢”、“代码大”、“堆内存不可控”。 这其实是对 C++98 的刻板印象。 在 Modern C++ 中,我们要区分两个概念:

  1. C with Classes:这是 C++ 的核心,性能与 C 完全一致。

  2. The Standard Library (STL):这才是导致“代码膨胀”的元凶(iostream, string, vector)。

真相: 一个不包含虚函数、不继承复杂层级、不使用异常的 C++ class,其内存布局与 C 的 struct 完全一致。 其成员函数(Member Function),仅仅是把 this 指针作为第一个参数传入的普通函数。

汇编对比

// C++
struct A { int x; void func() { x++; } };
// 汇编:LDR R0, [R0]; ADD R0, #1 (R0 是 this 指针)

// C
struct A { int x; }; void func(A* this) { this->x++; }
// 汇编:LDR R0, [R0]; ADD R0, #1

结论:零运行时开销 (Zero-Cost Abstraction)。


三、 C++ 的降维打击:四大核心武器

如果在单片机上使用 C++ (Strict Subset),你将获得 C 语言无法企及的优势。

1. RAII:彻底根治资源泄漏

在 C 语言中,管理锁、中断、GPIO 状态全靠人工:

// C 风格:极易出错
void ISR_Handler() {
    __disable_irq();
    if (Error) {
        // 完了,忘了开中断,系统死机
        return; 
    }
    __enable_irq();
}

C++ RAII 风格:利用栈对象的析构函数,自动化管理生命周期。无论怎么 return,中断一定会被恢复。(参考我之前的文章《RAII 资源管理》)。

2. 模板 (Templates) vs void*

C 语言做通用链表或队列,只能用 void*,既不安全又慢(无法优化)。 C++ 模板通过单态化 (Monomorphization),为每种类型生成一份专用代码。

// C++ 模板
template <typename T> void Sort(T* arr, int n);

// 当你调用 Sort<int> 和 Sort<float> 时
// 编译器生成了两个函数:Sort_int 和 Sort_float
// 虽然代码体积略微增加,但运行速度达到了极致(内联、特定指令优化)

降维打击:你获得了 Python 一样的泛型编程体验,却保留了汇编级的执行效率。

3. constexpr:把计算挪到编译期

C 语言的 const 只是只读变量,要在运行时占内存。 C++ 的 constexpr 可以在编译阶段执行复杂的数学运算(如生成正弦表、CRC 表),生成的二进制里只有结果,没有计算过程。这是一次对运行时性能的“白嫖”。

4. CRTP:静态多态 (Static Polymorphism)

这是最硬核的。C 语言实现驱动接口通常用函数指针(动态多态),有运行时开销。 C++ 利用 CRTP(奇异递归模板模式),可以在编译期确定调用关系。

// 编译器直接将 UART_Send 内联进来,就像你自己手写寄存器操作一样快
// 但你面对的是清晰的对象接口
uart.Send(0x55);

四、 嵌入式 C++ 的“避坑指南” (The Trap)

C++ 是一把双刃剑,用不好会切到手。在 MCU 上使用 C++,必须遵守 "No-No List"

  1. 禁用 RTTI (Run-Time Type Information)

    • 后果:给每个类增加类型信息字符串,Flash 瞬间爆炸。

    • 对策:编译器参数 -fno-rtti

  2. 禁用异常 (Exceptions)

    • 后果:为了支持 try-catch,编译器会生成庞大的堆栈展开表(Unwind Tables),代码膨胀 20%~50%,且抛出异常时的延迟不可控。

    • 对策:编译器参数 -fno-exceptions

  3. 慎用标准库容器

    • std::vector, std::map, std::string 均依赖 new/malloc

    • 对策:使用 ETL (Embedded Template Library) 或手写静态容器(如之前文章提到的 ObjectPool, FlatHashMap)。

  4. 注意 static 对象的构造

    • C++ 全局对象的构造函数会在 main 之前执行,这涉及到 .init_array 段的处理。如果启动文件(Startup Code)没写好,会导致系统无法启动。


五、 终极裁决:怎么选?

维度 C 语言 Modern C++ (Embedded Subset)
可读性 低 (宏、指针、全局变量) (类、命名空间、强类型)
抽象能力 弱 (面向过程) 极强 (面向对象、泛型、元编程)
编译速度 慢 (模板展开需要时间)
二进制体积 (只要不用异常和 RTTI)
运行时效率 极高 极高 (甚至更高,因为内联和 constexpr)
人才储备 极多 较少 (懂嵌入式 C++ 的是稀缺资源)
调试难度 简单 中等 (模板报错信息反人类)

结论

  1. 如果你在写 Bootloader、极小的 8 位机项目、或者维护古老的遗留代码:请坚持用 C。简单直接是第一生产力。

  2. 如果你在写 STM32/ESP32 复杂业务、IoT 协议栈、电机控制算法、机器人中间件:请大胆拥抱 C++

    • class 封装驱动。

    • template 复用算法。

    • constexpr 计算常量。

    • RAII 管理中断和锁。

C++ 不会让你代码变慢,只会让你的架构变强。是时候走出“C++ 不适合单片机”的舒适区了。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐