在 STM32 等嵌入式系统开发中,扎实的C语言功底是基石。以下梳理了面试或实际工作中高频出现的C语言问题,并附上代码示例与解析,帮助开发者查漏补缺。
1. 预处理指令 #define 的运用
如何用预处理指令#define声明一个常量,用以表示1年中的秒数(忽略闰年)?
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
解析:计算过程使用预处理器,而非运行时计算,效率更高。末尾的UL表示无符号长整型,确保常量在特定平台上的类型正确性。
如何编写一个标准宏MIN,用于返回两个参数中的较小值?
#define MIN(A,B) ((A) <= (B) ? (A):(B))
解析:宏定义中的每个参数都必须用括号括起来,以防止运算符优先级导致的意外错误。整个表达式也需要括号,确保宏作为一个整体使用时的正确性。
预处理器标识#error的目的是什么?
#error : 停止编译并显示错误信息
解析:当预处理器遇到#error指令时,会强制中止编译过程,并输出其后的错误信息。常用于检查不满足的编译条件,如特定的宏定义或版本要求。
2. 嵌入式系统中的无限循环
嵌入式系统中经常需要无限循环,如何使用C语言编写死循环?

如上图所示,常见的三种写法为:
while(1) {…}
do {…} while(1)
for(;;) {…}
3. 变量与指针的定义
请用变量a给出以下定义:
- 整型数:
int a;
- 指向整型数的指针:
int *a;
- 指向指针的指针,它指向的指针指向整型数:
int **a;
- 有10个整型数的数组:
int a[10];
- 有10个指针的数组,它的指针指向整型数:
int *a[10];
- 指向有10个整型数的数组的指针:
int (*a)[10];
- 指向函数的指针,该函数有一个整型参数,并返回一个整型数:
int (*a)(int);
4. static关键字的作用
static关键字在C语言中主要有三个作用:

- 在函数体内:修饰局部变量,使其生命周期变为整个程序运行期,但作用域不变(仅函数内可见),实现函数调用间的状态保持。
- 在模块内(.c文件内):修饰全局变量,使其作用域仅限于本模块(文件),无法被其他模块的文件访问。
- 在模块内:修饰函数,使其作用域仅限于本模块,成为该模块的“私有”函数。
5. const关键字的作用
const关键字用于定义只读变量,其主要作用包括:

- 定义只读常量:明确告知编译器和其他程序员,此变量的值不应被修改。
- 提高代码灵活性:配合指针使用,可以更精细地控制数据的可修改性。
- 提供编译期保护:编译器会阻止试图修改
const修饰参数的代码,防止无意的误操作。
const与指针结合的几种经典定义:

const int a; / int const a;:a是整型常量。
const int *a;:a是指向整型常量的指针(指针可变,指向的数据不可变)。
int * const a;:a是指向整型的常量指针(指针不可变,指向的数据可变)。
const int * const a;:a是指向整型常量的常量指针(指针和指向的数据都不可变)。
6. volatile关键字的作用与实例
volatile用于修饰变量,告知编译器该变量的值可能会被程序未知的因素改变(如硬件寄存器、操作系统中断、多线程共享)。优化器在每次使用该变量时,都必须从内存中重新读取其值,而不是使用寄存器中暂存的副本。
需要定义为volatile的变量场景示例:

- 并行设备的硬件寄存器(如状态寄存器)。
- 一个中断服务程序中会访问到的非自动变量(即全局变量)。
- 多任务或多线程应用中,被多个任务共享的变量。
7. 嵌入式系统位操作
嵌入式系统经常需要对硬件寄存器或变量进行位操作。例如,如何置位和清零一个变量的某一位?
#define BIT3 (0x08 << 3) // 定义第3位为1
static int a;
void set_bit3() {
a |= BIT3; // 将a的第3位置1
}
void clear_bit3() {
a &= ~BIT3; // 将a的第3位清零
}

8. 访问特定内存地址
有时需要直接访问特定的物理内存地址。例如,如何设置绝对地址0x67a9处的整型变量的值为0xaa66?
int *ptr = NULL;
ptr = (int *) 0x67a9; // 将指针指向绝对地址
*ptr = 0xaa66; // 向该地址写入数据

9. 中断服务程序(ISR)注意事项
中断是嵌入式系统的核心机制之一。当中断事件发生时,CPU暂停当前程序,转去执行中断服务程序(ISR),执行完毕后再返回。
在编写ISR时,需遵循以下规范:

- 不能有返回值:ISR应被声明为
void类型。
- 不能传递参数:ISR没有参数列表。
- 应保持短小精悍:避免在ISR中进行复杂的浮点运算,因为可能涉及上下文保存,效率低下且行为不确定。
- 避免调用不可重入函数:例如
printf(),它通常有重入性和性能问题,不应当在ISR中调用。
10. 有符号与无符号整型的隐式转换
下面这段代码的输出结果是什么?
void foo(void) {
unsigned int a = 6;
int b = -20;
(a + b > 6) ? puts(" > 6 ") : puts(" <= 6 ");
}

解析:当表达式中同时存在有符号和无符号类型时,C语言标准规定,所有操作数会被自动转换为无符号类型。因此,-20会被转换成一个很大的无符号整数。(a + b)的结果远大于6,所以最终输出为 “ > 6 ”。
11. 动态内存分配
在支持动态内存的嵌入式环境中,基本的分配与释放操作如下:
int *p = NULL;
p = (int *)malloc(sizeof(int) * 128); // 申请128个int大小的内存
free(p); // 使用完毕后释放内存
p = NULL; // 建议将指针置NULL,防止野指针

注意:在资源受限的嵌入式系统中,动态内存分配需谨慎,需注意内存泄漏和碎片问题。
12. typedef 与 #define 定义指针类型的区别
typedef用于为现有类型创建别名。在定义指针类型时,它与#define有显著区别。
#define dPS struct s *
typedef struct s * tPS;
dPS p1, p2; // 等价于 struct s * p1, p2; (p1是指针,p2是结构体变量)
tPS p3, p4; // 正确定义了两个结构体指针 p3 和 p4

解析:#define是简单的文本替换,dPS p1, p2;被展开为struct s * p1, p2;,这导致只有p1是指针,p2是一个结构体变量。而typedef创建了一个完整的新类型别名tPS,因此tPS p3, p4;明确定义了两个指针。
掌握这些C语言核心知识点,是进行高效、稳定嵌入式开发的前提。希望这份梳理能帮助大家巩固基础。如果你想深入探讨更多嵌入式或预处理器相关的技术细节,欢迎来云栈社区交流分享。