当你写好了一个c程序,点击vc等集成开发环境的运行按钮时,有没有好奇过,这磁盘上的test.c文件究竟是怎样被加工成可执行文件,又被加载到内存中去运行的?
编译前:
编译后运行:
在接下来的几篇文章里,我将结合实例,来详细说说这其中发生的故事。
概览
总体上来看,一个c语言程序,由磁盘上的一份源文件变成内存中的一个进程,会经历5个过程,分别是
- 预处理
- 编译
- 汇编
- 链接
- 加载
今天,我们先来看预处理。
预处理
预处理,其实就是在源代码被编译前,做一些文本性质的操作,这主要包括:
- 删除注释
- 插入#include语句包含的头文件内容
- 根据#define语句进行宏替换
- 根据#if 语句删除部分条件编译的内容
预处理器会根据#include语句指定的文件名,在特定的文件路径中去查找文件,然后用头文件的内容替换掉#include语句。
例:
源文件test.c:
预处理后的test.i文件:
在预处理后的文件中,原来的主函数内容没有变,只是上面多了几百行的头文件内容 。
2)宏替换:预处理器支持两种不同的#include语句:
1)#include<文件名>:
这通常用于声明一些库文件,此时的文件查找路径采用编译器定义的默认位置进行查找,在UNIX系统中,默认位置一般是/usr/include。
2)#include"文件名":
这通常用于我们自己定义的头文件,此时会先去源文件的当前目录进行查找,查找失败后再去默认位置进行查找。
宏替换,就是根据#define语句定义的规则,对源文件进行批量替换。
平时写程序,我们经常会为数字常量定义宏:
//源程序: test.c #include#define MAX_SIZE 100 int main(){ printf("%d",MAX_SIZE); return 0; } //预处理后的程序:test.i ...... int main(){ printf("%d",100); return 0; }
除了定义常量宏,我们还可以定义带参数的宏:
//源程序: test.c #include#define SUB(x,y) (x-y) int main(){ int a = 1; int b = 2; printf("%d",SUB(1,2)); return 0; } //预处理后的程序:test.i ...... int main(){ int a = 1; int b = 2; printf("%d",(1-2)); return 0; }
定义语句宏:
//源程序: test.c #include#define do_forever for(;;) int main(){ do_forever{ printf("Hello world");//无限打印hello world } return 0; } //预处理后的程序:test.i ...... int main(){ for(;;){ printf("Hello world"); } return 0; }
语句宏,可以让我们调试程序更加方便:
//源程序: test.c #include#define DEBUG_PRINT printf("x=%d,y=%d,z=%dn",x,y,z) int main(){ int x=1,y=2,z=5; for(int i=0;i<10;i++){ x*=2; y+=x; z=x+y; DEBUG_PRINT;//直接用宏插入调试语句,打印变量的中间值 } return 0; } //预处理后的程序:test.i ...... int main(){ int x=1,y=2,z=5; for(int i=0;i<10;i++){ x*=2; y+=x; z=x+y; printf("x=%d,y=%d,z=%dn",x,y,z); } return 0; }
我们甚至可以替换多行的语句:
//源程序: test.c #include#define PRINT_HELLO for(int i=0;i<10;i++){/ printf("Hello world");/ } int main(){ PRINT_HELLO return 0; } //预处理后的程序:test.i ...... int main(){ for(int i=0;i<10;i++){ printf("Hello world"); } return 0; }
但是像这样的多行替换,并不建议大家使用。如果程序中出现多次PRINT_HELLO,我们不如将其实现为一个函数。用宏去替换多行语句,会导致预处理后的程序有大量重复的代码。
3)条件编译:大家想想这样一个问题: 如果我们为了能多次调试程序,想要永久的保留调试语句,但又不想让程序在非调试状态时打印出debug信息,这时该怎么办呢?
//源程序: test.c #include#define DEBUG_PRINT printf("x=%d,y=%d,z=%dn",x,y,z) int main(){ int x=1,y=2,z=5; for(int i=0;i<10;i++){ x*=2; y+=x; z=x+y; DEBUG_PRINT;//不想永久删除它 } return 0; } //预处理后的程序:test.i ...... int main(){ int x=1,y=2,z=5; for(int i=0;i<10;i++){ x*=2; y+=x; z=x+y; printf("x=%d,y=%d,z=%dn",x,y,z); } return 0; }
针对此类问题,c语言给我们提供了一个方便的工具,这就是条件编译:
条件编译的格式:
#if 一个宏常量
语句
...
#elif 一个宏常量
语句
...
...
#else
语句
...
#endif
处理逻辑:
预处理器自上而下检查每个分支中的宏常量的值,值为1则保留该分支的语句,
删除其他分支,值都不为1则保留else分支的语句
利用条件编译,我们就可以通过控制宏常量的值,来优雅的保留调试语句了:
//源程序: test.c #include#define IS_DEBUG 0 #define DEBUG_PRINT printf("x=%d,y=%d,z=%dn",x,y,z) int main(){ int x=1,y=2,z=5; for(int i=0;i<10;i++){ x*=2; y+=x; z=x+y; #if IS_DEBUG DEBUG_PRINT; #ENDIF } return 0; } //预处理后的程序:test.i ...... int main(){ int x=1,y=2,z=5; for(int i=0;i<10;i++){ x*=2; y+=x; z=x+y; } return 0; }
除此之外,通过条件编译,我们还能很容易的用一份程序编译出两个不同的最终版本,比如:
普通版本:
//源程序: test.c
#define IS_VIP 0
void vip_service(){
......
}
void common_service(){
......
}
int main(){
#if IS_VIP
vip_service();
#else
common_service();
return 0;
}
//预处理后的程序:test.i
......
void vip_service(){
......
}
void common_service(){
......
}
int main(){
common_service();
return 0;
}
vip版本:
//源程序: test.c
#define IS_VIP 1
void vip_service(){
......
}
void common_service(){
......
}
int main(){
#if IS_VIP
vip_service();
#else
common_service();
return 0;
}
//预处理后的程序:test.i
......
void vip_service(){
......
}
void common_service(){
......
}
int main(){
vip_service();
return 0;
}
总结
预处理部分的内容到这里就讲完啦,让我们来复习下:
预处理过程主要包括:
- 删除注释
- 插入头文件中的内容
- <>声明:常用于库文件,会在默认目录查找
- “ “ 声明: 常用于自定义头文件,会先在当前源文件目录查找
- 宏替换
- 常量替换
- 带参替换
- 单行,多行语句替换
- 条件编译
- 用#if...#else...#endif条件分支的形式实现条件编译,预处理器通过检测每个分支下的宏常量的值,来选择性的保留或删除语句。
以上,就是本文的全部内容啦 ! 下篇文章,我将详细讲讲预处理后的第二个步骤——编译!
——————————————————手动分割————————————————————
彩蛋最后给大家看一个有趣的程序,大家思考下程序最后打印出的值是多少?欢迎大家与我交流,咱们下篇文章见!!!
#include#define MAX(a,b) ((a)>(b)?(a):(b)) int main(){ int x = 3; int y = 4; int z; z = MAX(x++,y++); printf("x =%d,y=%d,z=%d",x,y,z); return 0; }
(答案在评论区)



