1.7.1 计算机执行的简单实例
用计算机来求解一个问题时,通常是先根据问题建立数学模型,再将问题转化为在有限步内可以实现的算法,并选择合适的语言编写程序,接着进行编译、链接,形成可执行程序,运行并得到最终结果。例如,要求解12减3的差。那么可以编写如下的一个简单的求解程序。
【例1.1】 求解12-3的差。
首先用伪代码给出算法描述:
步骤1:用字符a、b分别表示整数12和3,即a=12,b=3;
步骤2:命令计算机执行减法运算a-b,得到运算结果c;
步骤3:将运算结果c从显示屏幕输出。
接着高级语言(本例用C语言)实现上述算法,文件名为Sub.c:
以上用C语言编写的程序,对于计算机硬件来说是无法识别的,因为计算机只能识别0/1的二进制代码,即机器语言。那么高级语言到机器语言的转换需要一个翻译工具来实现,这个翻译工具就是人们通常所说的编译器。编译器处理Sub.c的过程如下:
(1)首先通过程序的编译器输入Sub.c源文件,该文件在没有被编译之前每个字符是用ASCII码存放的。如图1.25所示,例如“#”字符对应的ASCII码就是23 H,“i”对应的ASCII码就是69 H。保存在计算机中就是文本文件。
图1.25 Sub.c源文件在计算机中存储的是ASCII码
预处理过程:读取Sub.c源程序,对其中的伪指令(以“#”开头的指令)和特殊符号进行处理。伪指令主要包括以下四个方面:
①宏定义指令,如#define Name TokenString,#undef等。
对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
②条件编译指令,如#ifdef、#ifndef、#else、#elif、#endif等。
这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
③头文件包含指令,如#include"File Name"或者#include<FileName>等。
采用头文件的目的主要是为了使某些定义可以供多个不同的C 源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条"#include"语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
包含到C源程序中的头文件可以是系统提供的,这些头文件一般被放在"/usr/include"目录下。在程序中"#include"要使用尖括号(<>)。另外,开发人员也可以定义自己的头文件,这些文件一般与C源程序放在同一目录下,此时在"#include"中要用双引号("")。
④特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE 标识将被解释为当前行号(十进制数)。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输入而被翻译成为机器指令。本阶段生成的文件为Sub.i文件。
(2)编译过程:在经过预编译得到的输出文件Sub.i文件中,只有常量如数字、字符串、变量的定义,以及C语言的关键字,如main、if、else、for、while等。编译程序所要做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的汇编代码。本阶段生成的汇编语言文件为Sub.s文件。
(3)汇编过程:汇编过程实际上把汇编语言代码Sub.s翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。本阶段生成的机器语言文件是Sub.o。
(4)链接过程:由汇编程序生成的目标文件Sub.o虽然是一个二进制文件,但并不能立即被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数等。所有的这些问题,都需要经链接程序的处理才能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。本阶段即可生成Sub.exe的可执行文件。至此编译工作就完成了,如图1.26所示。
图1.26 Sub.c编译过程