本篇文章介绍 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
我会单独用一篇博客进行详细讲解。