#error的作用是什么?
#error 是一条预处理指令,它的作用是让预处理器发出一条错误信息,并立即中断编译过程。这在代码条件编译中非常有用,可以用于检查不满足的配置或条件。
下面是一个从实际代码中简化而来的例子:
#include <stdio.h>
#define RX_BUF_IDX 100
#if RX_BUF_IDX == 0
static const unsigned int rtl8139_rx_config = 0;
#elif RX_BUF_IDX == 1
static const unsigned int rtl8139_rx_config = 1;
#elif RX_BUF_IDX == 2
static const unsigned int rtl8139_rx_config = 2;
#elif RX_BUF_IDX == 3
static const unsigned int rtl8139_rx_config = 3;
#else
#error "Invalid configuration for 8139_RXBUF_IDX"
#endif
int main(void)
{
printf("hello world\n");
return 0;
}

这段代码的逻辑很简单:它检查宏 RX_BUF_IDX 的值。只有当这个值在0到3之间时,才会定义对应的 rtl8139_rx_config 变量。如果 RX_BUF_IDX 被定义为其他值(比如这里的100),预处理器就会执行 #error 指令,输出错误信息 "Invalid configuration for 8139_RXBUF_IDX" 并停止编译。
我们编译一下看看实际效果:

可以看到,编译过程确实因为 #error 指令而失败了。这对于快速定位宏定义或配置错误非常高效。
位操作的基本使用
问题:如何用一个宏来给一个32位数据的特定位(bit)置1?
答案:
#define SET_BIT(x, bit) (x |= (1 << bit)) /* 置位第bit位 */
这个宏利用了位或(|=)和左移(<<)操作符。(1 << bit) 会生成一个只有第 bit 位是1,其他位都是0的掩码。然后通过 |= 操作将其与目标变量 x 合并,从而实现将 x 的第 bit 位设置为1的功能。
来看一个具体的代码示例和运行结果:
#include <stdio.h>
#define SET_BIT(x, bit) (x |= (1 << bit)) /* 置位第bit位 */
int main(void)
{
unsigned int a = 0x68; /* 二进制:01101000b */
printf("0x68的第2位置1后的值变为:%#x\n", SET_BIT(a, 2));
return 0;
}

编译并运行这个程序:

0x68 的二进制是 0110 1000,第2位(从0开始计数)是0,将其置1后变成 0110 1100,即十六进制的 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
原因分析:
这个结果初看可能有些反直觉,-20 + 6 怎么会大于6呢?关键在于C语言的隐式类型转换,有时也称为“寻常算术转换”。
当 unsigned int 和 int 进行运算时,编译器会将有符号数 b 转换为无符号数。也就是说,表达式 a + b 实际上等价于 a + (unsigned int)b。
在32位环境下,-20 转换为无符号数后的值是 0xFFFFFFFF - 20 + 1 = 4294967276。所以 a + b 就变成了 6 + 4294967276,其结果远远大于6。
C语言遵循一套明确的类型转换规则,当操作数类型不同时,排名靠后的类型会自动(隐式)转换为排名靠前的类型。常见的排名顺序(从高到低)可以简单理解为:
double > float > unsigned long > long > unsigned int > int
掌握这些隐式转换规则对于避免整数溢出、理解复杂表达式的结果至关重要。
typedef与#define的区别
typedef 和 #define 都可以用来为类型起别名,但它们有本质区别:
- 语法:
#define 之后不带分号,它是预处理指令;typedef 之后带分号,它是C语言语句。
-
类型扩展:#define 定义的别名可以用其他类型说明符进行扩展,而 typedef 定义的则不行。
#define INT1 int
unsigned INT1 n; // 没问题,等同于 unsigned int n;
typedef int INT2;
unsigned INT2 n; // 有问题!INT2已经被typedef定义为int的别名,不能再用unsigned修饰。
-
连续定义变量时的行为:这是最重要的区别之一。typedef 能保证定义的所有变量类型一致,而 #define 可能因为单纯的文本替换导致意外。
#define P_INT1 int*;
P_INT1 p1, p2; // 展开为 int* p1, p2; 即 p1是指针,p2是普通int变量。
typedef int* P_INT2;
P_INT2 p1, p2; // p1、p2 类型相同,都是指向int的指针。
在 #define 的例子中,p2 只是一个 int 变量,这可能不是开发者想要的结果。而 typedef 则清晰地定义了一个“指针类型”,用它声明的所有变量都是该类型的指针。
写一个MAX宏
最基础的 MAX 宏可以这样写:
#define MAX(x, y) ((x) > (y) ? (x) : (y))
用括号把参数 x 和 y 分别括起来,是为了防止当参数是复杂表达式时,因运算符优先级问题而产生错误。例如,如果没有括号,MAX(a & 0xFF, b & 0xFF) 可能会被错误地展开。
这个基础版本在日常使用中基本够用。但要写出更严谨、能处理各种边界情况(如参数带副作用、不同类型比较等)的 MAX 宏,则需要更高级的技巧,这常常是考察开发者对C语言理解深度的好题目。
死循环
在嵌入式系统或需要无限循环的场景中,常用的C语言死循环写法有以下几种:
-
while 循环:
while(1) {
// 循环体
}
这是最直观、最常用的写法。
-
for 循环:
for(;;) {
// 循环体
}
for 语句的三个表达式都留空,同样构成一个无限循环。
-
goto 循环:
Loop:
// 循环体
...
goto Loop;
使用 goto 和标签,这种方式在现代编程中不常用,但在某些底层或特定场景下可能会见到。
static的作用
在C语言中,关键字 static 主要有三个作用,取决于它所修饰的对象:
-
在函数体内修饰局部变量:
将一个局部变量声明为 static,会使该变量在程序的整个生命周期内都存在(存储在静态存储区),而不是在函数调用结束时被销毁。这意味着该变量在多次函数调用之间能保持其值不变,实现了局部变量的“记忆”功能。
-
在模块内(函数体外)修饰全局变量:
将一个全局变量声明为 static,会限制该变量的链接属性。它依然在程序的整个生命周期内存在,但它的作用域被限制在声明它的源文件(模块)内,其他源文件无法通过 extern 引用它。这可以看作是一个“文件内部的全局变量”,有助于信息隐藏和减少命名冲突。
-
在模块内修饰函数:
将函数声明为 static,同样限制了函数的链接属性。该函数只能在声明它的源文件内被调用,对其他源文件不可见。这常用于实现模块内部的辅助函数,避免污染全局命名空间。
const的作用
const 关键字用于定义常量,即其值在初始化后不能被修改。理解 const 与指针结合时的声明是关键。请看下面几个声明:
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
让我们逐一解读:
const int a; 和 int const a;:这两个声明是等价的,都表示 a 是一个常整型数,其值不可变。
- *`const int a;
**:这表示a是一个**指向常整型数的指针**。也就是说,指针a所指向的那个整型数据是常量,不能被修改(*a = 10;是非法的),但指针a本身可以指向别的地址(a = &b;` 是合法的)。
- *`int const a;
**:这表示a是一个**指向整型数的常指针**。指针a本身是常量,初始化后不能再指向其他地址(a = &b;是非法的),但它所指向的整型数据可以被修改(*a = 10;` 是合法的)。
- *`int const a const;
**:这是最严格的声明。它表示a是一个**指向常整型数的常指针**。既不能通过a修改它所指向的数据(*a = 10;非法),也不能修改a本身使其指向其他地址(a = &b;` 非法)。
记忆口诀:const 在 * 左边,修饰的是指向的数据;const 在 * 右边,修饰的是指针本身。
volatile的作用
volatile 是嵌入式系统编程中一个至关重要的关键字。它告诉编译器,这个变量的值可能会被程序之外的、不可预知的因素改变(例如硬件寄存器、中断服务程序、多线程环境中的其他线程)。因此,编译器在对这个变量进行优化时必须“小心谨慎”:
- 每次使用该变量时,都必须直接从其内存地址中重新读取,而不能使用之前可能存储在寄存器里的副本。
- 对该变量的任何写入操作,都必须立即写回内存。
哪些变量应该被声明为 volatile 呢?
- 并行设备的硬件寄存器(如状态寄存器、数据寄存器)。
- 一个中断服务子程序(ISR)中会访问到的非自动变量(即全局变量或
static 局部变量)。
- 多线程应用中被多个任务共享的变量。
不理解 volatile 可能导致灾难性的后果,比如读取不到硬件的最新状态,或者多线程数据不一致。因此,它常被用作区分普通C程序员和嵌入式系统程序员的标志性问题。
下面是一些深入的问题:
-
一个参数既可以是 const 还可以是 volatile 吗?
是的。一个典型的例子是只读的状态寄存器。它是 volatile 的,因为它的值可能被硬件随时改变;它也是 const 的,因为程序不应该、也不能去写这个寄存器。
-
一个指针可以是 volatile 吗?
是的。虽然不常见,但确实存在。例如,一个指向某缓冲区的指针,这个指针变量本身的地址可能被一个中断服务程序修改。
-
下面的函数有什么错误?
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
答案:这个函数意图返回 *ptr 指向值的平方。但由于 ptr 被声明为指向 volatile int 的指针,编译器会认为 *ptr 的值可能在两次取值之间被改变。因此,它可能会生成类似下面的、读取两次的代码:
int square(volatile int *ptr)
{
int a, b;
a = *ptr; // 第一次读取
b = *ptr; // 第二次读取
return a * b;
}
如果 *ptr 在两次读取之间真的发生了变化(比如被中断修改),那么 a 和 b 的值就会不同,函数返回的也就不是期望的平方值,而是两个不同值的乘积。
正确写法是先将易变的值存入一个局部变量,再用这个变量进行计算:
int square(volatile int *ptr)
{
int a;
a = *ptr; // 读取一次
return a * a; // 使用局部变量计算
}
变量定义
用变量 a 给出下面的定义,这是对C语言声明语法基本功的考察:
a) 一个整型数
b) 一个指向整型数的指针
c) 一个指向指针的的指针,它指向的指针是指向一个整型数
d) 一个有10个整型数的数组
e) 一个有10个指针的数组,该指针是指向一个整型数的
f) 一个指向有10个整型数数组的指针
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数
h) 一个有10个函数指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
答案:
a) int a;
b) int *a;
c) int **a;
d) int a[10];
e) int *a[10];
f) int (*a)[10];
g) int (*a)(int);
h) int (*a[10])(int);
其中,f)、g)、h) 涉及到数组指针和函数指针,是语法中的难点,需要仔细理解括号的作用。掌握这些定义是理解复杂声明和进行高级程序开发的基础。
中断函数
中断是嵌入式系统的核心组成部分。许多编译器提供了扩展关键字(如 __interrupt)来支持用标准C编写中断服务子程序(ISR)。评论一下下面这段 __interrupt 函数的代码:
__interrupt double compute_area(double radius)
{
double area = PI * radius * radius;
printf(" Area = %f", area);
return area;
}
这段代码存在多个问题:
- ISR 不能有返回值。中断处理函数通常是由硬件或操作系统异步调用的,没有合适的调用者来接收其返回值。因此,ISR 应声明为
void 类型。
- ISR 不能有参数。中断的发生是异步事件,没有标准的机制向ISR传递参数。获取信息通常需要通过查询硬件状态或访问全局变量。
- 在ISR中进行浮点运算通常是不可取的。首先,浮点运算在许多处理器上不是“可重入”的,且可能涉及额外的状态保存(如浮点寄存器入栈),这会增加中断响应时间。其次,ISR的设计原则是短小精悍、快速执行,耗时的浮点运算与此原则相悖。
- 在ISR中调用
printf() 等标准I/O函数是危险的。这些函数本身通常不可重入,且执行效率低下,可能导致性能问题甚至程序崩溃。在ISR中应避免任何可能引起阻塞或执行时间不确定的操作。
一个合格的嵌入式开发者必须对中断的特性有深刻理解。如果你想深入探讨嵌入式开发中的其他技术难点或面试问题,欢迎到云栈社区交流讨论。