本篇文章介绍 C 语言中类似
#define,#if,#ifdef等预处理指令以及宏的高级用法,最后整理出项目中一些常用的宏,例如打印调试信息等。
本篇文章将不会介绍简单的宏用法,例如#define ADD(a, b) ((a)+(b))
本篇文章大部分参考《C Primer Plus 第六版》第 16 章
一、预处理及宏
1.1 “#” 运算符
# 是一个预处理运算符,可以将记号转化成字符串。例如 #define TYPE(x) #x,若使用宏 TYPE(int),则将其替换成字符串 "int",#x 就是转换为 x 的形参名。
下面是一个例子。
1 |
|
输出结果为:The num 3 is an int type
1.2 “##” 运算符
与 # 运算符类似,## 运算符可以用于类函数宏的替换部分,而且还可以用于对象宏的替换部分。## 运算符将两个记号组合成一个记号,例如 #define TEST(n) TEST_##n,然后宏 TEST1 将其展开为 TEST_1。
下面是一个具体的例子。
1 |
|
注意,
#运算符组合成字符串,而##运算符组合成为一个新的标识符。
1.3 #undef 指令
#undef 指令用于取消已定义的 #define 指令。若之前没有定义某个宏,取消对其的定义也是有效的,如果想使用一个名称,但不确定之前是否已经用过,使用 #undef 先取消定义是一个安全的方法。
1.4 条件编译指令
1.4.1 #ifdef、#else 和 #endif 指令
先用一个简单的例子来说明这三个条件编译指令。
1 |
上述代码理解起来应该挺简单,若用 #define 定义了 MAVIS,就引入 horse.h 头文件,若没有定义 MAVIS 就引入头文件 cow.h。
#ifdef 测试的宏可以是对象宏,也可以是函数宏。
1.4.2 #ifndef
#ifndef 用法和 #ifdef 类似,但是意思相反。除此之外 #ifndef 还可以防止相同的宏被重复定义,例如下面的例子。
1 |
通过 #ifndef 也可以避免头文件被引入多次。
1.4.3 #if、#elif
#if 和 #elif 后面跟一个常量表达式,如果表达式的值为非零,则表达式为真,类似于 C 语言中的 if else,可以使用关系运算符和逻辑运算符。
#if 和 #elif 后面的宏只能是对象宏,不能是函数宏。
1.4.4 #defined
#defined 用于判断宏是否已经被定义,可以是对象宏,也可以是函数宏,可以和 #elif 嵌套使用。
条件编译可以让程序更容易移植,改变文件开头的几个关键定义,可以根据不同的架构或系统设置不同的值和包含不同的文件。
1.5 预定义宏
C 标准规定了一些预定义宏,如下列表格所示。
| 宏 | 含义 |
|---|---|
| DATE | 预处理的日期(“Mmm dd yyyy”形式的字面量,如 Nov 12 2023) |
| FILE | 表示当前源代码文件名的字符串字面量 |
| LINE | 表示当前源代码文件中行号的整型量 |
| STDC | 设置为 1 时表示遵循 C 标准 |
| STDC_HOSTED | 本机环境设置为 1,否则设置为 0 |
| STDC_VERSION | 支持 C99 标准,设置为 199901L;支持 C11标准,设置为 201112L |
| TIME | 翻译代码的时间,格式为 “hh:mm:ss” |
1.6 #line 和 #error
#line 指令重置 __LINE__ 和 __FILE__ 宏报告的行号和文件名,用法如下。
1 |
#error 指令让预处理器发出一条错误信息,该消息包含指令中的文本,用法如下。
1 |
编译上述代码将会产生 error,并且提示 Not C11。
1.7 变参宏 … 和 __VA_ARGS__
一些函数可以接受数量可变的参数,例如 printf,在头文件 stdvar.h 中提供了相关操作。
同样,宏定义中也可以实现可变参数,通过将宏列表中最后的参数写成 ... 来实现这一功能。这样,预定义宏 __VA_ARGS__ 可用在替换部分中,用来表示省略号代表什么。例如定义 #define PRINT(...) printf(__VA_ARGS__),调用宏 PRINT("Hello"),__VA_ARGS__ 展开为一个参数 Hello,调用宏 PRINT("My name is %s", name),__VA_ARGS__ 展开为两个参数 "My name is %s" 和 name。
1.8 attribute
GNU C 的一大特色就是 __attribute__ 机制。__attribute__ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute)。
具体内容请参见链接 C语言__attribute__的使用、attribute 机制详解
二、宏模板
由于 C 语言中库比较少,而一些比较基础的操作又无需通过函数实现,因此可以将一些基础功能写成宏进行展开,并集成到头文件中,在今后的项目中可以很方便的进行调用。
在这里我自己总结并整理了若干个常用的宏。
| 宏名称 | 功能 |
|---|---|
| LOG | 打印调试信息(带颜色) |
| UPPERCASE | 转化为大写字母 |
| LOWERCASE | 转化为小写字母 |
| FPOS | 获取结构体成员偏移量 |
| FSIZ | 获取结构体成员所占用字节数 |
| container_of | 根据成员指针、结构体类型、结构体成员名称获取结构体起始地址 |
| offsetof | 获取结构体成员偏移量 |
2.1 打印调试信息
调试信息是任何项目必不可少的内容,下面的宏可以在终端中输出带颜色的调试标签,方便观察错误和警告信息。
1 |
调用上面的 LOG 宏,可以看到结果如下。
1 | int main() { |

2.2 大小写转化
1 |
2.3 得到一个结构体成员 member 在结构体 struct 中的偏移量
1 |
2.4 得到一个结构体中某个成员字段 member 所占用的字节数
1 |
2.5 container_of
1 |
container_of 宏函数的作用是 已知结构体 type 的成员 member 的地址 ptr,得到结构体 type 的起始地址。
第一行用于“类型检查”。它确保 type 有一个名为 member 的成员(不过我认为这也是由 offsetof 宏完成的),并且如果 ptr 不是指向正确类型(成员的类型)的指针,编译器将打印警告,这对调试很有用。
在上述宏的第三行,用了 char * 进行指针转化,这是因为 offsetof 指针偏移量是按照字节计算的,同时 char * 的指针也是以字节计算的,若转化为例如 int * 等类型,则 C 的指针算法将会计算 sizeof(int) * offsetof 作为最终的结果,也就是 4 字节乘以偏移量。
具体说明参考链接 container of()函数简介 和 linux 内核宏container_of剖析
2.6 offsetof
1 |
offsetof 宏函数的作用是 得到结构体 type 的成员 member 所在的内存偏移量
对于 container of 以及 offsetof 我会单独用一篇博客进行详细讲解。