转载前注明出处,欢迎转载分享
程序执行前
程序执行前经历了:
预处理器->编译器->汇编器->链接器
1)预处理过程:
- 处理所有注释,以空格代替
- 将所有#define删除,并展开所有宏定义
- 处理条件编译指令#if,#ifdef,#elif,#else,#endif
- 处理#include,展开被包含的文件
- 保留编译器需要使用的#pragma指令
预处理指令:
1
|
gcc |
2)编译过程:
- 对预处理文件进行一系列词法分析,语法分析和语义分析
- 分析结束后进行代码优化生成相应的汇编代码文件
编译指令:
1
|
gcc |
3)汇编过程:
- 汇编器将汇编代码转变为机器可以执行的指令
- 每个汇编语句几乎都对应一条机器指令
汇编指令:
1
|
gcc |
注意:因为.o文件内部为机器码(即二进制文件),所以生成的.o文件无法打开
4)链接器:
- 链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接。
静态链接:将库文件与.o文件绑定在一起(可能一个库拷贝给多个可执行程序)
缺点:占内存
动态链接:库文件在内存中存放,是共享的而不是程序的一部分,生成的.o文件在内存中找库文件然后生成可执行程序
缺点:每次执行程序需要找库文件,虽然节省内存,但是比静态链接耗时很多
小结:
编译器将编译工作主要分为预处理,编译和汇编三部
链接器的工作是把各个独立的模块链接为可执行程序
静态链接在编译期完成,动态链接在运行期间完成
---------------------------------------------------------------------------------------------------
宏表达式
- 宏表达式在预编译期被处理,编译器不知道宏表达式的存在
- 宏表达式用“实参”完全替代形参,不进行任何运算
- 宏表达式没有任何的“调用”开销
- 宏表达式不能出现递归定义
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include #define int { } int { } |
上述代码则体现该宏定义的优势是函数无法实现的,函数形参intarray[]在接收参数的时候被当做是指针的形式,所以函数返回的结果为1,但是
宏只是简单的替换,计算出的结果就是数组元素的个数。
宏定义的常量或表达式是否有作用域的限制?看如下代码:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include int { #define } int { } int { } |
通过运行结果可知该代码说明宏定义不存在作用域的限制,如果我们想要对上述代码中宏定义进行作用范围的限制,则在上述代码中去掉注释,
加入#undefMIN,即可限定该宏在f1中可用,此时运行上述代码时会提示f2中MIN未定义。
强大的内置宏:
宏日志示例代码:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include #include #define } void { } int { } |
头文件time.h所包含的库函数和库宏:
1
|
#define |
对于上面的定义所代表的意思:
宏是简单的替换,实际上只有紧挨#define的那一块被替换为后面的部分即是将f替换成(x)((x)-1),尽管后面由多个空格隔开也不要紧,因为当宏经过预处理被替换到代码中时,我们知道空格不影响代码的运行。
------------------------------------------------------------------------------------------------
条件编译
条件编译的
基本概念:
- 条件编译的行为类似于C语言中的if..else
- 条件编译是预编译指示命令,用于控制是否编译某段代码
如下代码即为条件编译:
1
2 3 4 5 6 7 8 9 10 11 |
#include int { #if(C #else #endif } |
我们可以用
gcc -E test.c -otest.i生成预编译文件,然后查看这段代码时main函数只有第一个printf,上述代码与普通的if..else语句不同点在于其处理在预编译时期即改变。但是如果用gcc-DC=0 -E test.c -otest.c时我们会发现预编译时保留的是第二个printf语句,这里说一下
-DC=0的意思,即是将C定义为0。
#include的本质是将已经存在的文件内容嵌入到当前文件中
#include间接包含同样会产生嵌入文件内容的动作
那么间接包含同一个头文件就会产生编译错误,因为可能某个头文件声明的全局变量被声明多次,例如在global.h中声明int a=10;然而test.h头文件中也包含了global.h头文件,那么当test.c中既包含了global.h又包含了test.h的时候,则相当于调用了两次global.h,声明了两次全局变量a,那么肯定会出现编译错误,改进格式如下:
1
2 3 4 5 6 |
#ifndef #define //头文件内容 #endif |
保持如上格式后即可随心所欲调用头文件多次而不产生重复和报错。
条件编译的意义:
- 条件编译使得我们可以按不同的条件编译不同的代码段,因而可以产生不同的目标代码
- #if..#else..#endif被预编译器处理;而if..else语句被编译器处理,必然被编译进目标代码
- 实际工程中条件编译主要用于以下两种情况:
不同的生产线公用一份代码(同一份代码可以产生不同生产线)
区分编译产品的调试版和发布版
用于不同生产线的示例代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include #ifndef #define #else #define #endif #ifdef void { } #else void { } #endif int { #ifdef #else #endif } |
切换至DEBUG模式:
1
|
gcc |
运行结果:
[test.c:22] enter main()...
1.query information.
2.record information.
3.delete information.
4.exit.
[test.c:30] exit main()...
如果切换至普通模式:
1
|
gcc |
运行结果:
1.query information.
2.record information.
3.delete information.
4.exit.
切换至高级模式:
1
|
gcc |
运行结果:
this is the high level product!
1.query information.
2.record information.
3.delete information.
4.high level query.
5.mannul service.
6.exit.
还可以切换至高级DEBUG模式:
[test.c:22] enter main()...
this is the high level product!
1.query information.
2.record information.
3.delete information.
4.high level query.
5.mannul service.
6.exit.
[test.c:38] exit main()...
小结:
- 通过编译器命令行能够定义预处理器使用的宏
- 条件编译可以避免重复包含头同一个头文件
- 条件编译是在工程开发中可以区别不同产品线的代码
- 条件编译可以定义产品的发布版和调试版
--------------------------------------------------------------------------------------------------
#error
用于生成一个编译错误消息,并停止编译
用法:
1
|
#error |
注:message不需要用双引号包围
#error编译指示字用于自定义程序员特有的编译错误消息
类似的,#warning用于生成编译警告,但不会停止编译
示例代码:
1
2 3 4 5 6 7 8 9 10 11 12 |
#include int { #ifndef #warning #error #endif } |
如果直接编译:gcc test.c,则会报错停止。
若果编译时进行宏定义:
1
|
gcc
|
即意思是定义宏COMMAND为字符串"TestCommand"(我们可以在预处理.i文件中查看其变化),此时编译运行正常。
#line
用于强制指定新的行号和编译文件名,并对源程序的代码重新编号
用法:
1
|
#line |
注:filename可省略
#line编译指示字的本质是重定义__LINE__和__FILE__
示例代码:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include #line void { } int { } |
代码结果为:
20
Hello.c
因为从#line的下一行开始本来应该为第4行,但我们指定下一行为14行,所以打印__LINE__的行号原本为第10行,现在则变为第20行。并且__FILE__打印出来的是我们用#line宏所指定的文件名。
-----------------------------------------------------------------------------------------------
#和##运算符的使用解析
#运算符
我们知道#是预处理指令的开始符,但是它还有另一种作用,#运算符还可以用于在预编译期将宏参数转换为字符串。
示例代码:
1
2 3 4 5 6 7 8 9 10 11 12 13 |
#include #define int { } |
输出结果:
Hello world!
100
while
return
分布编译的话查看.i文件时,main中输出语句为:
printf("%s\n", "Hello world!");
printf("%s\n", "100");
printf("%s\n", "while");
printf("%s\n", "return");
宏还可以在调用函数之前打印所调用函数的名称(利用#打印字符串的作用来实现)
代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include #define int { } int { } int { } |
生成.i文件后主函数两条输出语句变为:
printf("1. %s\n", (printf("Call function %s\n", "square"),square(4)));
printf("2. %s\n", (printf("Call function %s\n", "f"),f(10)));
##运算符
用于在预编译期粘连两个符号,示例代码:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include #define int { } |
输出结果:
1
2
通过单步编译生成.i文件查看其预处理时发现主函数如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 |
int { } |
以上为##粘连符的使用方法,看如下代码巧妙宏定义结构体
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include #define struct STRUCT(Student) { } int { } |
预处理后的结构体实际是:
1
2 3 4 5 6 |
typedef struct { } |