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