程序的执行时有两种环境,一种是翻译环境,另一种是执行环境。程序先经过编译成为obj的后缀的文件,然后将文件和链接库链接起来,然后将形成可执行程序,前者时翻译环境,后者时执行环境。(链接库就是库函数的所在和一些其他的)
1.翻译环境
翻译分为编译和链接,编译又分为预编译,编译和汇编;链接又分为合并段表和合并符号表和重定位。
预编译就是将一些引用的头文件转换和,#definde定义的进行替换,注释的删除等,就可以得到预编译的程序文件(后缀时.i)
编译就是将预编译的文件由C语言代码转变为汇编代码,编译器要进行语法分析,词法分析,符号汇总,语义分析的操作,得到后缀为.s的编译文件。其中语法分析就是将整个程序中函数,全局变量等进行汇总。
汇编就是将汇编代码转变成二进制指令和形成符号表,把汇编文件转变成以obj结尾的目标文件。这个符号表就是将每一个obj目标文件就是将符号汇总中进行匹配地址,就可以得到符号表。(如果没有函数体只是函数声明,就匹配一个无效地址)
链接的合并段表就是将不同文件(obj文件是linux elf文件的格式,内容是一段一段的)每一段进行合并。
链接的符号表的合并和重定位,就是将不同文件中的符号表进行合并,相同的符号,删除无效地址的符号,得到全新的符号表。重新根据符号的地址定位,最终得到可执行程序。
2.执行环境
执行环境的具体过程是
(1)将文件载入内存中;
(2)调用main函数
(3)执行程序代码和函数堆栈,存储函数的局部变量和返回地址;
(4)终止程序(有可能是意外终止和正常终止)
下面介绍预处理的详解
3.预处理符号
在编译器中自带一些符号,他们可以显示编译的一系列信息,相当于存储这些信息的变量:
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号,就是显示当前代码所在编译器的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
下面是使用示例
\是续行符,可以将代码延申到下一行。
4 .#define
(1)#define定义标识符
define可以定义常量,代码等,本质上是替换,将定义的替换成原来的,在进行汇编。
如下
(2)#define定义宏
宏就是一种类似函数形式,本质上是将宏参在文本中进行替换。然后将文本在主函数中进行替换后运算。
宏的声明方式是#define name( parament-list ) stuff,其中parament-list是宏参,是传给宏的参数,stuff是文本,然后就会进行文本宏参的替换和代码的替换。
下面是使用示例:
经过替换后可以写成
int main()
{printf("%d", ((2)+(3)));return 0;}
经过完整的替换,文本是什么样就替换成什么样,这一点很重要!!!
我们为了防止出现出乎意料的结果,我们要每一个宏参加括号,整个文本加括号。
宏参可以出现其他#define定义的变量,但是不可以出现递归,字符串常量时不被检索。
宏和函数的对比:
函数
函数代码只出现于一个地方,每次使用这个函数时,都调用那个地方的同一份代码 存在函数的调用和返回的额外开销,所以相对慢一些
函数参数只在函数调用的时候求值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测。
函数参数只在传参的时候求值一 次,结果更容易控制。
函数的参数是与类型有关的,如 果参数的类型不同,就需要不同的函数,即使他们执行的任务是 不同的。
函数是可以逐语句调试的
函数是可以递归的
#define定义宏
每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长
宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括号。
参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。
参 数 类 型 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。
宏是不方便调试的
宏是不能递归的
(3)#和##
#是将非字符转变成字符
##是将两个符号合并成一个符号,这样的连接产生已申明的标识符,否则无效。
下面是使用示例
#和##只能在宏中使用。
(4)#undef
#undef就是将定义的宏进行删除,当我们需要重命名宏,发现重复时,就可以使用#undef来删除宏。