目录
一、学习目标
二、宏定义
预处理
宏的概念
带参宏
无值宏定义
三、条件编译
条件编译
条件编译的使用场景
四、头文件
头文件的作用
头文件的内容
头文件的基础语句:
GCC编译器的4个编译步骤:
总结
一、学习目标
- 掌握宏定义含义和用法
- 理解条件编译的场景和用法
- 清楚头文件的概念和用法
二、宏定义
预处理
在C语言程序源码中,凡是以井号(#)开头的语句被称为预处理语句,这些语句严格意义上并不属于C语言语法的范畴,它们在编译的第一个阶段统一由所谓预处理器(cc1)来处理。所谓预处理,顾名思义,指的是真正的C程序编译之前预先进行的一些处理步骤,这些预处理指令包括:
- 头文件:#include
- 定义宏:#define
- 取消宏:#undef
- 条件编译:#if、#ifdef、#ifndef、#else、#elif、#endif
- 显示错误:#error
- 修改当前文件名和行号:#line
- 向编译器传送特定指令:#progma
- 基本语法
- 一个逻辑行只能出现一条预处理指令,多个物理行需要用反斜杠( \ 转义字符)连接成一个逻辑行
- 预处理是整个编译全过程的第一步:预处理 - 编译 - 汇编 - 链接
- 可以通过如下编译选项来指定来限定编译器只进行预处理操作 -E:
gcc example.c -o example.i -E
宏的概念
宏(macro)实际上就是一段特定的字串,在源码中用以替换为指定的表达式。例如:
#define PI 3.14
此处,PI 就是宏(宏一般习惯用大写字母表达,以区分于变量和函数,但这并不是语法规定,只是一种习惯),是一段特定的字串,这个字串在源码中出现时,将被替换为3.14。例如:
int main()
{
printf("圆周率: %f\n", PI);
// 此语句将被替换为:printf("圆周率: %f\n", 3.14);
}
- 宏的作用:
- 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。
- 使得程序修改更易行:修改宏定义,即修改了所有该宏替换的表达式。
- 提高程序的运行效率(宏函数):程序的执行不再需要函数切换开销,而是就地展开。
无参宏
无参宏意味着使用宏的时候,无需指定任何参数,比如:
#define PI 3.14
#define SCREEN_SIZE 800*480*4
int main()
{
// 在代码中,可以随时使用以上无参宏,来替代其所代表的表达式:
printf("圆周率: %f\n", PI);
mmap(NULL, SCREEN_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, ...);
}
注意到,上述代码中,除了有自定义的宏,还有系统预定义的宏:
// 自定义宏:
#define PI 3.14
#define SCREEN_SIZE 800*480*4
// 系统预定义宏
#define NULL ((void *)0)
#define PROT_READ 0x1 /* Page can be read. */
#define PROT_WRITE 0x2 /* Page can be written. */
#define MAP_SHARED 0x01 /* Share changes. */
#define NULL ((void *)0)
宏的最基本特征是进行直接文本替换(不做任何的运算以及判断),以上代码被替换之后的结果是:
int main()
{
printf("圆周率: %f\n", 3.14);
mmap(((void *)0), 800*480*4, 0x1|0x2, 0x01, ...);
}
带参宏
带参宏意味着宏定义可以携带“参数”,从形式上看跟函数很像(因此带参宏被称为宏函数),例如:
#define MAX(a, b) a>b ? a : b
#define MIN(a, b) a<b ? a : b
以上的MAX(a,b) 和 MIN(a,b) 都是带参宏,不管是否带参,宏都遵循最初的规则,即宏是一段待替换的文本,例如在以下代码中,宏在预处理阶段都将被替换掉:
int main()
{
int x = 100, y = 200;
printf("最大值:%d\n", MAX(x, y));
printf("最小值:%d\n", MIN(x, y));
// 以上代码等价于:
// printf("最大值:%d\n", x>y ? x : y);
// printf("最小值:%d\n", x<y ? x : y);
}
- 带参宏的特点:
-
- 直接文本替换,不做任何语法判断,更不做任何中间运算。
- 宏在编译的第一个阶段就被替换掉,运行中不存在宏。
- 宏将在所有出现它的地方展开,这一方面浪费了内存空间,另一方面有节约了切换时间。
带参宏的副作用
由于宏仅仅做文本替换,中间不涉及任何语法检查、类型匹配、数值运算,因此用起来相对函数要麻烦很多。例如:
#define MAX(a, b) a>b ? a : b
int main()
{
int x = 100, y = 200;
printf("最大值:%d\n", MAX(x, y==200?888:999));
}
直观上看,无论 y 的取值是多少,表达式 y==200?888:999 的值一定比 x 要大,但由于宏定义仅仅是文本替换,中间不涉及任何运算,因此等价于:
printf("最大值:%d\n", x>y==200?888:999 ? x : y==200?888:999);
可见,带参宏的参数不能像函数参数那样视为一个整体,整个宏定义也不能视为一个单一的数据,事实上,不管是宏参数还是宏本身,都应被视为一个字串,或者一个表达式,或者一段文本,因此最基本的原则是:
- 将宏定义中所有能用括号括起来的部分,都括起来,比如:
#define MAX(a, b) ((a)>(b) ? (a) : (b))
无值宏定义
定义无参宏的时候,不一定需要带值,无值的宏定义经常在条件编译中作为判断条件出现(实现条件编译),例如:
#define BIG_ENDIAN
#define __cplusplus
三、条件编译
条件编译
- 概念:有条件的编译,通过控制某些宏的值,来决定编译哪段代码。
- 形式:
- 形式1:判断表达式 MACRO 是否为真,据此决定其所包含的代码段是否要编译
- 注意:#if形式条件编译需要有值宏
#define A 0
#define B 1
#define C 2
#if A
... // 如果 MACRO 为真,那么该段代码将被编译,否则被丢弃
#endif
// 二路分支
#if A
...
#elif B
...
#endif
示例:
#define DEBUG 0
#define DEMO 1
#define TEST 0
int main(int argc, char const *argv[])
{
printf("%d__%s\n" , __LINE__ , __FUNCTION__ );
printf("%d__%s\n" , __LINE__ , __FUNCTION__ );
// 判断宏 DEBUG是否为真
#if DEBUG
printf("%d__%s\n" , __LINE__ , __FUNCTION__ );
#endif
// 判断宏 DEMO 是否为真 来决定使用的是 21行或23行
#if DEMO
printf("%d__%s\n" , __LINE__ , __FUNCTION__ );
#else
printf("%d__%s\n" , __LINE__ , __FUNCTION__ );
#endif
// 判断宏 DEMO 是否为真 如果为真则启用地29行,否则判断TEST是否为真来决定启用 31或33行
#if DEMO
printf("%d__%s\n" , __LINE__ , __FUNCTION__ );
#elif TEST
printf("%d__%s\n" , __LINE__ , __FUNCTION__ );
#else
printf("%d__%s\n" , __LINE__ , __FUNCTION__ );
#endif
return 0;
}
如何在编译时定义或指定宏的值:
gcc xxx.c -DDEBUG=1 # 编译xxx.c时设置宏DEBUG的值为1
- 形式:
- 形式2:判断宏 MACRO 是否已被定义,据此决定其所包含的代码段是否要编译
// 单独判断
#ifdef MACRO
...
#endif
// 二路分支
#ifdef MACRO
...
#else
...
#endif
- 形式:
- 形式3:判断宏MACRO是否未被定义,据此决定其所包含的代码段是否要编译
// 单独判断
#ifndef MACRO
...
#endif
// 二路分支
#ifndef MACRO
...
#else
...
#endif
- 总结:
- #ifdef 此种形式,判定的是宏是否已被定义,这不要求宏有值。
- #if 、#elif 这些形式,判定的是宏的值是否为真,这要求宏必须有值。
- 在命令行中进行编译程序时可以直接使用-D选项来定义宏,此时宏的值如果没有特意给那么该宏的值默认为真。
gcc xxx.c -o xxx -DDEBUG # 当前命令行中定义了一个宏 DEBUG并该宏没有赋值那么他的值默认为真
条件编译的使用场景
控制调试语句:在程序中,用条件编译将调试语句包裹起来,通过gcc编译选项随意控制调试代码的启停状态。例如:
gcc example.c -o example -DMACRO
以上语句中,-D意味着 Define,MACRO 是程序中用来控制调试语句的一个宏,如此一来就可以在完全不需要修改源代码的情况下,通过外部编译指令选项非常方便地控制调试信息的启停。
选择代码片段:在一些大型项目中(例如 Linux 内核),某个相同功能的模块往往有不同的实现,需要用户根据具体的情况来“配置”,这个所谓的配置的过程,就是对代码中不同的宏的选择的过程。
#define A 0 // 网卡1
#define B 1 // 网卡2 √
#define C 0 // 网卡3
// 多路分支
#if A
...
#elif B
...
#elif C
...
#endif
四、头文件
头文件的作用
通常,一个常规的C语言程序会包含多个源码文件(.c),当某些公共资源需要在各个源码文件中使用时,为了避免多次编写相同的代码,一般的做法是将这些大家都需要用到的公共资源放入头文件(.h)当中,然后在各个源码文件中直接包含即可。
头文件的内容
- 头文件中所存放的内容,就是各个源码文件(xxx.c)的彼此可见的公共资源,包括:
- 全局变量的声明。
- 普通函数的声明。
- 静态函数的定义。 --- 使用static 修饰的函数
- 宏定义。
- 结构体、联合体的声明。
- 枚举常量列表的声明。
- 其他头文件。 --- 嵌套包含其他 的头文件
头文件的基础语句:
#ifndef MY_HEAD_H //这是一个条件编译,判断MY_HEAD_H是否没有被定义 (如果没定义则条件成立)
#define MY_HEAD_H //定义一个宏 MY_HEAD_H
// 错误示范, 如果该头文件被多个.C所包含则有可能会导致重复定义的问题
int a = 123 ;
char b = 45 ;
#endif //ifndef 的结束标记
注意:
- 头文件中的基础格式 #ifndef #define #endif 这3条语句用于防止同一个头文件被一个.c源文件多次重复包含。
- 错误示范中 a与b的定义在多个原文件中被定义,因此在链接阶段会出现多重定义的问题。
- 特别说明:
- 全局变量、普通函数的定义一般出现在某个源文件(*.c *.cpp)中,其他的源文件想要使用都需要进行声明,因此声明语句一般放在头文件中更方便。
- 静态函数、宏定义、结构体、联合体的声明都只能在其所在的文件可见,因此如果多个源文件都需要使用的话,放到头文件中定义是最方便,也是最安全的选择。
GCC编译器的4个编译步骤:
预处理:
编译:
汇编:
该操作直接使用汇编器,把上一个步骤产生的.s汇编文件直接转换为对应的二进制的指令。
链接
总结
本文细讲了C语进阶路上的终极BOSS关卡,这次的BOSS可以结合之前的所有技能和特点,C语的第一阶段到现在就结束了,将各关卡的小怪全部斩杀后可获得大量经验值。而下一阶段来细讲数据结构的小怪、BOSS,听说通过后有神秘奖励哦~最后祝各位都可爬上C语巅峰,斩尽拦路小妖。
本文参考 粤嵌文哥 的部分课件,经过整理和修改后发布在C站。如有转载,请联系本人