【C语言笔记】宏定义
1. C语言编译过程
C语言作为一个高级编程语言,由它所编写的源代码,是需要经过编译后生成二进制可执行文件,才能被计算机上的执行者所执行。使用编译器将C语言编写的源代码翻译成二进制可执行文件的过程就称为编译过程,这个过程主要分为四个阶段:预处理、编译、汇编、链接。
1.1.预处理
编译过程的第一步预就是预处理,预处理器会将源文件进行预操作,识别源代码中的预处理指令关键字,删除所有的注释,在处理结束后会产生一个默认后缀为.i
的临时文件,如若使用gcc
编译器,可使用以下命令对源文件仅进行预编译操作。1
gcc -E ./source_code.c -o source_code.i
预处理命令关键字都是以#
符号开头,后面拼接关键单词组成。 常用预处理命令关键字
关键字 名称 用法 作用 #include 嵌入文件命令 #include
“file_path”用于将引号中所指示的文件嵌入到当前编译单元中,文件的搜寻路径和编译的设置相关 #define 宏定义命令 #define
identifier content用于将某个内容使用其标识符代替,在预处理过程中,预处理器会将标识符替换为所表示的内容,这一过程称为宏展开 #undef 取消上文宏定义命令 #undef
identifier用于取消编译单元下方宏定义的标识符所代表的内容,在该预处理命令的上文中,标识符还是有效的 #error 强制编译停止命令 #error
message当预处理器遇到此命令时,会停止编译,并向编译窗口输出message的内容 #warning 发出警告信息命令 #warning
message当预处理器遇到此命令时,会向编译窗口输出message的内容,根据编译器设置的编译类型来决定是否停止编译 #if、#else、#elif、#endif、#ifdef、#ifndef 条件编译命令 #if
constant-expression根据常量表达式的结果来选择性进行编译包含的代码块 #line 设定行号或文件名命令 #line
number[“filename”]改变 __LINE__
与 __FILE__
的内容,filename是可选的参数#pragma 指示编译器命令 #pragma
once在该编译单元中,此文件仅会被嵌入一次
1.2. 编译
在编译阶段,编译器首先会检查源文件的语法是否符合规范,以确定代码实际的逻辑,再将编译单元翻译成汇编代码。查看汇编代码,也是定位C语言程序的问题的重要手段。在处理结束后会生成一个默认后缀为.s
的临时文件,如若使用gcc
编译器,可使用以下命令对源文件进行编译操作生成汇编代码文件。1
gcc -S source_code.i -o source_code.s
1.3. 汇编
汇编阶段是把编译阶段生成的.s
文件转化成目标文件。目标文件是二进制文件,不能直接查看其内容,但可以借助readelf、objdump等工具知晓其内容。在处理结束后会生成一个默认为.o
的临时文件,如若使用gcc
编译器,可使用以下命令生成目标文件。1
gcc -c source_code.s -o source_code.o
1.4. 链接
在成功编译之后,就进入了链接阶段。C语言的最小编译单元就是一个后缀为.c
的源代码文件,但在一个项目中,往往会将不同功能的实现划分在不同的源码文件中,在将每个源码文件编译成目标文件中,需要将其链接到一起,生成一个具有完整上下文的可执行文件。链接阶段不仅包含目标文件之间的链接,还有函数库的链接。总之,可执行文件中的所有符号在链接过程中都能找到其定义。
函数库一般分为静态库和动态库两种。静态库是指在编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不需要库文件了,其后缀一般为“.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时链接文件加载库,这样就可以节省系统的开销,动态库一般后缀名为“.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。
如若使用
gcc
编译器,可使用以下命令生成可执行文件。1 | gcc source_code.o -o hello_world |
2.宏
2.1. 什么是宏定义
宏可以看作为一些命令的集合。它是一种预处理器命令,在预处理阶段将宏名替换为后面的内容体。
在C语言中,可以使用#define
来定义宏:1
e.g. 宏定义示例
1 |
|
上述代码中,在预处理阶段结束后,CIRCLE_Perimeter
和CIRCLE_Area
宏会被展开,其内容如下。1
2
3
4
5
6
7
8
9
10
11int main(int argc, char **argv)
{
float radius1 = 10.0;
float radius2 = 20.0;
float Perimeter1 = (2*10.0*(3.1415926));
float Perimeter2 = (2*20.0*(3.1415926));
float CIRCLE_Area1 = (10.0*10.0*(3.1415926));
float CIRCLE_Area2 = (20.0*20.0*(3.1415926));
return 0;
}
2.2. 宏展开过程
在编写复杂的嵌套宏定义时,不了解宏展开的具体过程,很容易使写出的宏定义在编译时报错。以下是宏的展开过程。
- 宏参数和
#define
定义中可以出现其他#define
定义的符号。但是对于宏,不能出现递归。 - 当预处理器搜索
#define
定义的符号的时候,字符串常量的内容并不被搜索。
1 |
|
2.3. 常用的宏定义
以下为我在工作中经常会用到的宏定义,我这里整理了一份。
lang.h
1 | /*⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄ |
args.h
1 | /*⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄ |
attributed.h
1 | /*⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄ |
objectd.h
1 | /*⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄ |
self_section.h
1 | /*⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄ |