重点:程序的翻译环境、程序的执行环境、详解:C语言程序的编译+链接、预定义符号介绍 、预处理指令 #define 、宏和函数的对比 、预处理操作符#和##的介绍 、命令定义 、预处理指令 #include 、预处理指令 #undef 、条件编译
一 程序的翻译环境和执行环境
不同的编译器,对于缓冲区实现的方式不同。
在 ANSI C的任何一种实现中,存在两个不同的环境: 第 1 种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。第2种是执行环境,它用于实际执行代码。
二 详解编译+链接
2.1 翻译环境
翻译环境分成两个部分:编译和链接;(C语言源代码通过编译生成目标文件(.obj/.o)(多个源文件单独通过编译器生成各自的目标文件),目标文件加上链接库通过链接器 链接成为可执行程序(.exe))
编译过程:预编译、编译、汇编
2.2 编译本身也分为几个阶段
预编译(预处理)——编译——汇编 (预编译也叫作预处理)
预处理之后产生的结果都放在test.i文件中,编译完成之后产生的结果保存在test.s中,汇编完成之后产生的结果保存在test.o或者是test.obj(在linux中是.o;Windows中是.obj)。
预编译:(1)进行头文件的展开(2)删除注释(3)#define定义的符号替换(例如:#define Max 100,在预编译结束之后,Max会被替换为100,并把#define这一行给删掉)
总而言之,就是进行一些文本操作。
编译(把C语言代码转换成汇编代码): (1)语法分析(2)词法分析(3)语义分析(4)符号汇总(全局符号:例如:函数名、main等)
汇编(把汇编代码转换成二进制的指令):(1)形成符号表(全局符号给一个地址,所有全局符号+地址形成符号表)
目标文件(.o)加上链接库通过链接器 链接成为可执行程序(.exe))
linux中.o目标文件以及可执行文件的文件格式是elf
链接(编译之后生成.o文件,加上链接库通过链接器进行链接):(1)合并段表(2)符号表(不同文件的符号表)的合并以及重定义
链接的时候,多个目标文件进行链接的时候会通过符号表,查看来自外部的符号是否真实存在
2.3 运行环境
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用 main 函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack ),存储函数的局部变量和返回 地址。程序同时也可以使用静态( static )内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
4. 终止程序。正常终止 main 函数;也有可能是意外终止。
三 预处理详解
这里讲述的内容都是在预处理阶段
3.1 预定义符号
__FILE__ // 进行编译的源文件
__LINE__ // 文件当前的行号
__DATE__ // 文件被编译的日期
__TIME__ // 文件被编译的时间
__STDC__ // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义 (这个并不是所有的编译器都支持)
例子:
1. #include <stdio.h> 2. int main() 3. { 4. printf("%s\n", __FILE__);//编译出来的结果,会显示运行文件的路径 5. printf("%d\n", __LINE__);//编译出来的结果,显示该行的行数303 6. printf("%s\n", __DATE__);//显示时间 7. printf("%s\n", __TIME__);//显示日期 8. return 0; 9. }
记录杂志:
1. #include <stdio.h> 2. #include <string.h> 3. #include <errno.h> 4. int main() 5. { 6. int i = 0; 7. FILE* pf = fopen("log.txt", "w"); 8. if (pf == NULL) 9. { 10. printf("%s\n", strerror(errno)); 11. return 0; 12. } 13. for (i = 0; i < 10; i++) 14. { 15. fprintf(pf, "%s %s %s %d %d\n", __DATE__, __TIME__, __FILE__, __LINE__, i); 16. } 17. fclose(pf); 18. pf = NULL; 19. return 0; 20. }
3.2 #define
3.2.1 #define 定义标识符
语法: #define name stuff (注意:定义完之后,最后不需要分号(;))
#define MAX 1000
#define reg register // 为 register 这个关键字,创建一个简短的名字
#define do_forever for(;;) // 用更形象的符号来替换一种实现
#define CASE break;case // 在写 case 语句的时候自动把 break 写上。
// 如果定义的 stuff 过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠 ( 续行符 ) 。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
3.2.2 #define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏( macro )或定义宏(define macro )。
#define name( parament - list ) stuff
其中的 parament - list 是一个由逗号隔开的符号,它们可能出现在 stuff 中。
注意: 参数列表的左括号必须与name 紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
1. #include <stdio.h> 2. #define SQUARE(x) x * x 3. //这也是个替换,用后面的内容替换前面的宏 4. 5. int main() 6. { 7. int a = 5; 8. printf("%d\n", SQUARE(a + 1));//打印结果为11 9. return 0; 10. }
解析:打印结果,我们会理所应当的以为是36,但是结果却是11,因为代入后是a + 1* a+1,所以结果是11,要是想让结果为36,正确的应该是#define SQUARE(x) ((x) * (x))
注意:定义宏,一定要注意括号,记得带上(大部分情况是需要带的,否则会出现优先级问题,导致想要的结果和预期的不一样)
比如上述代码,我们想要的是36,但是结果却是11
每一个部分带上括号,整体也要带上括号
3.2.3 #define 替换规则
先替换,后计算
在程序中扩展 #define 定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索(意思就是printf("M = %d", 10)中的M就不会被替换)。