error的作用是什么?
#error 指令用于在预处理阶段强制引发一个错误,并输出指定的错误信息,从而中断整个编译过程。它通常用于检查宏定义的合法性或确保代码在满足特定条件时才能编译。
例如,下面这段从Linux驱动代码简化而来的示例,演示了如何使用 #error 进行配置校验:

这段代码的逻辑很清晰:宏 RX_BUF_IDX 被定义为100。预处理器会依次判断其值,由于100不在0~3的范围内,程序便会执行 #else 分支下的 #error 指令,并输出错误提示信息 “Invalid configuration for 8139_RXBUF_IDX”。
我们编译一下看看实际效果:

可以看到,编译过程在预处理阶段就被中断了,错误信息正是我们在代码中通过 #error 定义的字符串。这种机制在驱动开发、跨平台代码的条件编译中非常有用,能及早发现不匹配的配置。
位操作的基本使用
面试官问:“如何用宏给一个32位数据的指定比特位置1?”
这是一个非常经典的嵌入式C语言面试题,考察对位操作和宏定义的基本功。一个标准的实现如下:
#define SET_BIT(x, bit) (x |= (1 << bit)) /* 置位第bit位 */
我们来写个简单的程序验证一下:

程序中将无符号整数 a 初始化为 0x68(二进制 01101000b),然后使用宏 SET_BIT(a, 2) 将其第2位(从0开始计数)设置为1。理论上,01101000b 的第2位置1后变为 01101100b,即 0x6C。
编译并运行,结果符合预期:

隐式转换规则
来看一道容易出错的题,以下代码的输出结果是什么?为什么?
#include <stdio.h>
int main(void)
{
unsigned int a = 6;
int b = -20;
if (a + b > 6)
printf("a+b大于6\n");
else
printf("a+b小于6\n");
return 0;
}
程序输出结果为:
a+b大于6
这个结果可能会让一些初学者感到意外。原因在于C语言的 “寻常算术转换” 规则。当 unsigned int 和 int 进行运算时,int 类型的操作数 b 会被隐式转换为 unsigned int 类型。
因此,表达式 a + b 实际上等价于 a + (unsigned int)b。在32位环境下,-20 转换为无符号数后是一个很大的正数(0xFFFFFFFF - 20 + 1 = 4294967276),所以 a + b 的结果远远大于6。
C语言的隐式转换遵循一个固定的优先级顺序:
double > float > unsigned long > long > unsigned int > int
当不同类型的数据进行运算时,排名靠后的类型会自动转换为排名靠前的类型。
typedef与#define的区别
typedef 和 #define 都可以用来为类型起别名,但两者有本质区别,是面试中的高频考点。
- 语法差异:
#define 是预处理指令,末尾不加分号;typedef 是关键字,语句结尾需要分号。
-
扩展性:#define 定义的别名可以用其他类型说明符进行扩展,而 typedef 定义的则不行。
#define INT1 int
unsigned INT1 n; // 正确,等同于 unsigned int n;
typedef int INT2;
unsigned INT2 n; // 错误,语法错误
-
处理时机与效果:#define 在预处理时进行简单的文本替换,typedef 则在编译时被视为真正的类型别名。这在连续定义变量时尤为关键:
#define PINT1 int*
PINT1 p1, p2; // 展开为 int *p1, p2; p1是指针,p2是int变量
typedef int* PINT2;
PINT2 p1, p2; // p1和p2都是int型指针
显然,使用 typedef 能保证所有变量的类型一致性,更为安全可靠。
写一个“安全”的MAX宏
如何编写一个求两者最大值的宏?最基本的版本是这样的:
#define MAX(x, y) ((x) > (y) ? (x) : (y))
这里用括号将每个参数和整个表达式括起来,是为了避免因运算符优先级导致的错误。例如,没有括号的 MAX(a & 0xFF, b & 0xFF) 展开后会出错。
然而,这个“基础版”宏仍然存在隐患:如果参数是带有副作用的表达式,例如 MAX(++a, ++b),则 a 或 b 可能会被求值两次,导致非预期的结果。
要写出真正严谨、安全的宏需要更多技巧,例如使用GCC的语句表达式 (({ ... })) 或内联函数。对于追求代码健壮性的嵌入式开发,理解这些高级用法很有必要。
死循环的写法
在嵌入式系统中,主程序或某些任务常常需要无限循环。用C语言实现死循环有几种常见方式:
- while 循环:
while(1) {
// 循环体
}
- for 循环:
for(;;) {
// 循环体
}
- goto 语句:
Loop:
// 循环体
goto Loop;
其中 while(1) 和 for(;;) 最为常用,goto 的方式在现代编程中较少使用,但在某些底层代码或特定场景下仍会出现。
static关键字的作用
在C语言中,static 关键字根据其修饰对象的不同,有三种主要作用:
- 在函数体内修饰局部变量:该变量的生命周期被延长至整个程序运行期,但作用域不变,仅在函数内可见。每次函数调用,该变量会保持上一次修改后的值(即具有“记忆性”)。
- 在文件作用域(函数体外)修饰全局变量:该变量仅在定义它的源文件内可见,其他源文件无法通过
extern 引用。这实现了信息的隐藏,是模块化编程的重要手段。
- 在文件作用域修饰函数:该函数的作用域被限制在定义它的源文件内,成为“内部函数”,防止其他文件调用。这同样有助于模块化设计和避免命名冲突。
const关键字的作用
const 用于定义常量,但其与指针结合时,位置不同含义差异巨大。请解释下列声明的含义:
const int a; // (1)
int const a; // (2)
const int *a; // (3)
int * const a; // (4)
int const * a const;// (5) 实际应写作: const int * const a;
- (1) 和 (2):作用相同,
a 是一个值不可修改的整型常量。
- (3):
a 是一个指向整型常量的指针。指针指向的整数值不可变,但指针本身可以指向其他地址。
- (4):
a 是一个指向整型的常量指针。指针本身(存储的地址)不可变,但它指向的整数值可以改变。
- (5):
a 是一个指向整型常量的常量指针。指针本身和它指向的整数值都不可改变。
记忆口诀:const 修饰其左侧的内容;如果左侧无内容,则修饰其右侧的内容。(3)中const修饰int(*a的类型),(4)中const修饰a(指针变量本身)。
volatile关键字的作用与陷阱
volatile 是一个至关重要的关键字,它告诉编译器该变量可能被程序之外的未知因素(如硬件、中断、其他线程)改变,因此编译器不应对其做任何优化假设,每次使用都必须从内存中重新读取其值。
典型的 volatile 变量应用场景包括:
- 并行设备的硬件寄存器(如状态寄存器)。
- 中断服务程序中访问的非自动变量(全局变量)。
- 多线程应用中被多个任务共享的变量。
不理解 volatile 的程序员很难胜任嵌入式开发工作。面试官常常会追问以下几个问题:
-
一个变量可以同时是 const 和 volatile 吗?为什么?
可以。例如一个只读的状态寄存器。它是 volatile 因为其值可能被硬件意外改变;它是 const 因为程序不应试图去写入它。
-
指针可以是 volatile 吗?为什么?
可以。例如一个中断服务子程序修改一个指向缓冲区的指针时,该指针变量就应该被声明为 volatile。
-
下面的函数有什么问题?
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
*问题在于`ptr可能被意外改变**。由于ptr指向一个volatile` 位置,编译器可能会生成类似下面的代码:
int a, b;
a = *ptr; // 第一次读取
b = *ptr; // 第二次读取,此时*ptr的值可能已与第一次不同!
return a * b;
这样返回的可能就不是期望的平方值了。正确的写法是先将值存入一个临时变量:
int square(volatile int *ptr)
{
int val = *ptr;
return val * val;
}
复杂变量声明解析
用变量 a 给出下面的定义,这是检验你对 C语言 声明语法理解深度的经典考题:
a) 一个整型数
int a;
b) 一个指向整型数的指针
int *a;
c) 一个指向指针的指针,它指向的指针是指向一个整型数
int **a;
d) 一个有10个整型数的数组
int a[10];
e) 一个有10个指针的数组,该指针是指向一个整型数的
int *a[10];
f) 一个指向有10个整型数数组的指针
int (*a)[10];
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数
int (*a)(int);
h) 一个有10个函数指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
int (*a[10])(int);
理解这些声明需要掌握“右左法则”:从变量名 a 出发,先向右看,再向左看,如此反复。
中断服务程序(ISR)的编写注意事项
在嵌入式系统中,中断服务程序 (ISR) 的编写有严格的限制。下面是一段有问题的 ISR 示例,使用了编译器扩展关键字 __interrupt:
__interrupt double compute_area(double radius)
{
double area = PI * radius * radius;
printf(" Area = %f", area);
return area;
}
这段代码存在多处不符合ISR规范的地方:
- 返回值:ISR 不应返回任何值,通常应声明为
void 类型。
- 参数传递:ISR 通常不接收参数。硬件中断发生时,参数传递机制与普通函数不同。
- 浮点运算与可重入性:在许多架构上,浮点运算不是可重入的,且可能涉及复杂的上下文保存(如浮点寄存器入栈),在要求快速响应的ISR中进行浮点运算是危险且低效的。
- 调用不可重入函数:
printf() 这样的标准库函数通常是非可重入的,并且非常耗时,绝对不能在ISR中调用。这可能导致数据损坏、死锁或严重的性能问题。
一个合格的ISR应该短小精悍,只做最必要的操作(如设置标志、读取数据),并将耗时处理交由主循环或任务完成。
以上就是对一系列经典嵌入式C语言面试题的解析与总结。掌握这些核心概念,不仅能帮助你在技术面试中游刃有余,更是编写稳定、高效嵌入式代码的基石。希望这篇梳理能对你有所帮助。更多深度技术讨论,欢迎访问 云栈社区 。