4. C++在嵌入式中的基础:编译流程、内存管理、指针与引用、面向对象基础

好,咱们进入第四章。这一章我打算聊聊C++在嵌入式里的几个基本功。说实话,很多做嵌入式的老手,一开始都是C语言的死忠粉。我当年也一样,觉得C++太“重”了,不适合单片机。直到后来项目越来越复杂,我才发现,用C++的某些特性,代码反而更清晰、更不容易出错。

这一章,我会把C++编译流程、内存管理、指针与引用、还有面向对象基础,这几个核心点串起来讲。你想想看,搞懂这些,你才算真正入了嵌入式C++的门。

4.1 C++编译流程:从源码到机器码

很多初学者觉得编译就是点一下“Build”按钮。其实没那么简单。C++的编译流程,说白了就是四步走:预处理、编译、汇编、链接。

第一步:预处理

预处理器处理以 # 开头的指令。比如 #include#define#ifdef。它会把这些宏展开,把头文件内容“复制粘贴”进来。我见过有人在一个头文件里写了上千行宏定义,结果预处理后,源文件膨胀了好几倍。嗯,编译时间自然就长了。

第二步:编译

这一步才是真正的“翻译”。编译器把预处理后的C++代码,翻译成汇编语言。它会做语法检查、语义分析,还有各种优化。比如,你写了个 for 循环,编译器可能会把它展开,或者用寄存器代替内存访问。

第三步:汇编

汇编器把汇编代码转成机器码,生成目标文件(.o 或 .obj)。目标文件里是二进制指令,但还不能直接运行。因为它里面可能调用了其他文件里的函数,这些地址还是“空的”。

第四步:链接

链接器把多个目标文件和库文件“粘”在一起。它会解析符号,比如你调用了 printf,链接器就会去标准库里找到它的地址,然后填到你的代码里。最终生成可执行文件。

我的小经验: 在嵌入式开发中,我习惯用 -c 选项只编译不链接,这样可以单独检查每个源文件的语法错误。等所有文件都编译通过了,再统一链接。这样定位问题快很多。

4.2 内存管理:堆、栈、静态区

嵌入式系统内存有限,所以内存管理是重中之重。C++程序的内存,主要分三个区域:栈、堆、静态区。

栈(Stack)

栈是自动管理的。函数里定义的局部变量,都放在栈上。函数一返回,这些变量就自动销毁了。栈的特点是:分配速度快,但空间小。我见过有人在一个函数里定义了一个大数组,结果栈溢出了,程序跑飞了。所以,大数组、大结构体,别放栈上。

堆(Heap)

堆是手动管理的。用 new 分配的内存,就在堆上。用完后必须用 delete 释放。堆的特点是:空间大,但分配速度慢,而且容易产生内存碎片。

静态区(Static Storage)

全局变量、静态变量,都放在静态区。它们在程序启动时分配,程序结束时才释放。静态区的生命周期是整个程序运行期间。

区域 分配方式 生命周期 典型问题
自动 函数内 栈溢出
手动 new/delete 控制 内存泄漏、碎片
静态区 自动 程序全程 多线程竞争
避坑指南: 我曾经在一个项目中,用 new 分配了一个对象,但忘记 delete 了。结果程序跑了几天后,内存耗尽,系统崩溃。从那以后,我养成了一个习惯:每次 new 之后,立刻写对应的 delete,或者直接用智能指针。

4.3 指针与引用:别搞混了

指针和引用,是C++里最容易搞混的两个概念。我刚开始学的时候,也经常犯迷糊。其实,它们有本质区别。

指针(Pointer)

指针是一个变量,它存储的是另一个变量的地址。你可以改变指针指向的地址,也可以改变指针本身的值。说白了,指针就是“间接访问”。

int a = 10;
int* p = &a;  // p 指向 a
*p = 20;      // 通过 p 修改 a 的值
p = nullptr;  // p 可以指向空

引用(Reference)

引用是另一个变量的别名。它必须在定义时初始化,而且一旦绑定,就不能再改变。引用本身不占内存(至少从语义上讲),它只是原变量的一个“外号”。

int a = 10;
int& r = a;  // r 是 a 的引用
r = 20;      // 修改 r 就是修改 a
// int& r2;  // 错误!引用必须初始化

什么时候用指针,什么时候用引用?

我个人习惯是:如果参数可能为空,或者需要改变指向,就用指针。如果参数一定不为空,而且只是用来访问对象,就用引用。比如,重载操作符时,通常用引用。

关键区别:
  • 指针可以重新赋值,引用不行。
  • 指针可以为空(nullptr),引用不行。
  • 指针有多级(如 int**),引用只有一级。

4.4 面向对象基础:类、封装、继承、多态

面向对象是C++的核心。在嵌入式里,用得好,代码复用性极高。用不好,反而增加代码体积和运行开销。

类与对象

类就是“蓝图”,对象就是“实物”。比如,你可以定义一个 LED 类,然后创建多个 LED 对象,每个对象控制一个实际的LED灯。

class LED {
public:
    void on() { /* 开灯 */ }
    void off() { /* 关灯 */ }
private:
    int pin;  // 引脚号
};

LED led1;  // 对象
LED led2;  // 另一个对象

封装

封装就是把数据和操作数据的方法,打包在一起。外部只能通过公开的接口访问,内部细节被隐藏。这样,你修改内部实现时,不会影响外部代码。

继承

继承可以复用已有的类。比如,你可以定义一个 RGB_LED 类,继承自 LED 类,然后添加颜色控制的功能。但要注意,继承会增加代码的耦合度。我建议,能用组合就别用继承。

多态

多态允许你用父类的指针或引用,调用子类的方法。在嵌入式里,多态通常通过虚函数实现。但虚函数会引入虚函数表(vtable),增加内存开销。所以,在资源极度受限的MCU上,要慎用。

我的建议: 在嵌入式C++中,我通常只使用封装和简单的继承。多态用得很少,除非是像状态机这种必须的场景。因为虚函数的性能开销,在实时性要求高的地方,可能会成为瓶颈。

好了,这一章的内容就这些。C++在嵌入式里的基础,说白了就是这几块。编译流程让你知道代码怎么变成机器码;内存管理让你知道内存怎么分配和释放;指针和引用让你知道怎么间接操作数据;面向对象让你知道怎么组织代码。把这些搞懂了,后面的章节,咱们就可以聊更深入的话题了。