编译环境
预处理
预处理是文本操作,主要进行以下三个操作。
- 将注释替换成空格,也可以说是将注释删除
- 将头文件所包含的内容展开
- 将#define所定义的符号进行替换
编译
编译:将c语言代码翻译成汇编代码。
- 语法分析
- 词法分析
- 语义分析
- 符号汇总(符号汇总所汇总的符号是全局的,例如自定义函数名,和main函数)
汇编
汇编:把汇编代码翻译成二进制指令,并生成.o文件(目标文件)。同时将汇总的符号生成符号表。
链接
链接:链接目标文件和链接库生成可执行程序(二进制的程序)
- 合并段表
- 符号表的合并与重定位
运行环境
程序运行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始,接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈,存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序(正常终止main函数,也有可能是意外终止)
预处理指令
#define
#define是所定义的符号或宏都是直接替换。例如#define M 5,那么在后续的代码中M会直接被替换成5
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。
#define定义宏
可能有人会好奇,为什么要添加如此多的括号。接下来我一一举例。
如图,我们预想的结果应该是36,即为3x(3x4),但因为#define是直接替换,所以导致其变成了3*2+1*1+3,按照优先级先算了乘法。所以要在x*y加上括号变成(x*y)。在以下代码中,即便改成(x*y)也还不行,替换时变成了3x(2+1*1+3),括号内部仍未变成我们想要的3x4,括号内的结果为6。所以正确的做法应该是改成((x)*(y)).
另外,像x+,++x,x--,--x这类自加自减的参数万不可扔进宏里,天知道他会自加自减多少次。
宏与函数的区别
把宏名全部大写,函数名不要全部大写。
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码。 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些。 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是 相同的。 |
调试 | 宏可以调试,但调试起来不方便。 | 函数是可以逐语句调试的。 |
递归 | 宏是不能递归的。 | 函数是可以递归的 |
#undef
#undef用于移除一个宏定义
如图所示,因为移除了add的定义,所以12行处变成了未定义标识符。
#include
- 本地文件包含:#include "stdio.h"
- 查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标 准位置查找头文件。 如果找不到就提示编译错误。
- 库文件包含:#include <stdio.h>
- 查找策略:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
库文件也可以用""来包含,但不推荐,查找的效率会下降。