在ANSIC的任何一种实现中,存在两个不同的环境。
第一种是翻译环境,在这个环境中源代码被转换成可执行的机器指令。第二种是执行环境,它用于实际执行代码。
翻译阶段的几个步骤:
1.组成一个程序的每个源文件通过编译过程分别转换成目标代码。
2.每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要函数也链接到程序中。
而编译本身也分为几个阶段组成:(预处理,编译,汇编,链接)
1.预处理器处理——预编译
在这个阶段,预处理器在源代码上执行一些文本操作。分别会进行宏替换,去注释,头文件展开(按路径),条件编译。
2.源代码经过解析,判断它的语句的意思。这个阶段产生绝大多数的错误和警告。(编译器角度)
3.然后产生目标代码。
在编译单元生成之后,便是将编译单元进行编译,生成目标文件(*.obj),对于主流的编译过程,通常存在两个阶段:首先是生成汇编语言,然后使用汇编器生成机器语言。机器语言顾名思义就是0101这样的二进制代码。生成机器语言的过程就是将MOV和AX和BX原封不动的用0101替换掉,如MOV代码是25,AX是01,BX是10,则翻译出来的机器码就是350110,二进制也就是001101010000000100010000。
接下来的任务是链接,生成可执行文件。链接的任务就是生成可执行文件。
程序的执行过程:
1.程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存中来完成。
2.程序的执行便开始。接着便调用main函数。
3.开始执行程序代码。这个时候程序将使用一个运行时的堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4.终止程序。正常终止main函数;也有可能是意外终止。
下面我想介绍一下预处理标识符(在预处理阶段进行处理)
__FILE__ 进行编译的源文件 __LINE__ 文件当前的行号
__DATE__ 文件被编译的日期 __TIME__ 文件被编译的时间
# 以#开头的指令,也称伪指令,通常这类指令是在预处理阶段进行处理的。伪指令主要包含三个方面:
1.宏定义指令,如#define Name TokenString,#undef等。对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的 Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
2.条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3.头文件包含指令,如#include "FileName"或者#include <FileName>等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用 头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再 在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号 (<>)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。
## ##结构则执行一种不同的任务。它把位于它两边的符号连接成一个符号。作为用途之一,它允许宏定义从分离的文本段创建标识符。
#define paster( n ) printf( "token"#n" = %d\n", token##n )
int token9 = 100;
再调用 paster(9);宏展开后token##n直接合并变成了token9。整个语句变成了
printf( "token""9"" = %d", token9 );
在C语言中字符串中两个相连双引号会被自动忽略,于是上句等同于
printf("token9 = %d", token9);。
即输出token9 = 100
宏和函数的区别
宏可以非常频繁地用于执行简单的计算,比如在两个表达式中寻找其中较大(或较小)的一个:#define MAX(a,b) ((a)>(b)?(a):(b))
为什么不用函数来完成,有两个原因:(1)用于调用和从函数返回的代码很可能比实际执行这个小型计算的工作代价更大,所以使用宏比使用函数在程序的规模和速度更胜一筹;(2)更重要的是,函数的参数必须声明为一种特定的类型,所以它只能在类型合适的表达式上使用。反之,上面的宏可以用于整型,长整型,单浮点型,双浮点型以及其他任何可以使用>操作符比较值大小的类型。换句话说,宏与类型是无关的。
和函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝将插入程序中,除非宏非常短,否则使用宏可能会大幅增加程序的长度。还有一些任务无法使用函数实现。例如:
#define MALLOC(n,type)
((type*)malloc((n)*sizeof(type)))
int *pi = MALLOC(25,int);
这个宏的第二个参数是一种类型,它无法作为函数参数进行传递。