程序编译过程

编译器Compiler),是一种电脑程序,它会将用某种编程语言写成的源代码(原始语言),转换成另一种编程语言(目标语言)。

它主要的目的是将便于人编写,阅读,维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序,也就是可执行文件。编译器将原始程序(Source program)作为输入,翻译产生使用目标语言(Target language)的等价程序。源代码一般为高阶语言 (High-level language), 如 Pascal、C、C++、C# 、Java 等,而目标语言则是汇编语言或目标机器的目标代码(Object code),有时也称作机器代码(Machine code)。

 一个现代编译器的主要工作流程如下: 源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 汇编程序 (assembler) → 目标代码 (object code) → 链接器 (Linker) → 可执行文件 (executables)

主要分为四个过程:

一。预处理阶段。

功能:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。

比如hello.c中第1行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。

结果就得到了另一个C程序,通常是以.i作为文件扩展名。

所有的C编译器软件包都提供了预处理器。编译C程序时,程序首先由编译器中的预处理器进行处理。在大多数C编译器中,预处理器都被集成到编译器程序中。当您运行编译器时,它将自动运行预处理器。

 

预处理器根据源代码中的指令(预处理器编译指令)对源代码进行修改。预处理输出修改后的源代码文件,然后,该输出被用作下一个编译步骤地输入。

 

1.#define 预处理器编译指令

   #define将一种文本替换为另一种文本。例如,要将text1替换为text2,可以这样编写代码:

#define text1 text2

   上述编译指令导致预编译器将源代码文件中的所有text1替换为text2,但如果text1位于双引号中,则不替换。替换宏最常见的用途是用于创建符号常量。例如:

#define MAX 1000

X = y * MAX;

Z = MAX – 12;

则在预处理中,上述源代码被修改为:

X = Y * 1000;

Z = 1000 – 12;

    这相当于使用编译其的“查找并替换”功能,所有的MAX替换为1000。当然,原来的源代码文件保持不变,而是创建一个副本,并进行修改。#define并不局限于创建数值符号常量。例如:

#define Myprintf printf

Myprintf(“hello, world.”);

    也可以使用编译指令#define来创建函数宏。函数宏是一种简写,使用简单的东西来表示复杂的东西。其名称中之所以包含“函数”两字,是因为这种宏能够接受参数,就像真正的函数那样。函数红的优点之一是,其参数对类型不敏感。因此,您可以将任何数值类型的变量传递给接受数值参数的宏。例如下面的预处理器编译指令:

#define HALFOF(value) ((value)/2)

    定义了一个名为HALFOF的宏,该宏接受一个名为value的参数。预处理器将源代码中的所有HALFOF宏替换为相应的定义文本,并在必要时插入参数。因此,下面的代码行:

   result = HALFOF(10);

将被替换为:

   result = ((10)/2);

同样,下面的代码行:

   printf(“%f”, HALFOF(x[1] + y[2]));

将被替换为:

   printf(“%f”, ((x[1] + y[2])/2));

使用函数还是宏?

源代码中的所有宏调用都将根据宏定义被扩展为相应代码。如果程序调用某个宏100次,则最后的程序中将包含100个扩展宏代码的拷贝;而函数代码只有一个拷贝。因此,就代码长度而言,使用函数更好。

程序调用函数时,转到函数处执行以及从函数返回涉及到一定的处理开销;而调用宏没有任何处理开销,因为宏代码被直接嵌入到程序中。因此,就速度而言,使用函数宏更好。

究竟是用那种,需要在速度和长度之间进行折中。

 

2.使用编译指令#include

遇到编译指令#include时,预处理器读取指定文件,并将其插入该编译指令所在的位置。在编译指令#include中,不能使用通配符*或?来包含一组文件,但可以嵌套编译指令#include。大多数编译器都对嵌套深度有一定的限制,对于支持ANSI标准的编译器,通常嵌套深度可达15层。

在编译指令#include中指定文件名的方式有两种。如果文件名用尖括号括起,如#include<stdio.h>,则预处理器将首先在标准目录中查找该文件,如果没有找到或没有指定标准目录,则编译器将在当前目录中查找。

什么是标准目录?在DOS中,是环境变量INCLUDE指定的目录。有关DOS环境的完整信息,请参阅DOS文档。通常,使用SET命令来设置环境变量(这通常是在autoexec.bat文件中设置的)。大多数编译器在安装时,将自动在autoexec.bat文件中设置INCLUDE变量。

另一种指定要包含的地文件的方法是,用双引号将文件名括起:#include “myfile.h”。在这种情况下,预处理器将在被编译的源代码文件所在的目录(而不是标准目录)中查找。一般而言,您编写的头文件应保存在源代码文件所在的目录中,并使用双引号将其括起。而标准目录只用于保存编译器自带的头文件。

 

3.使用#if、#elif、#else和#endif

   这四个预处理器编译指令用于控制有条件的编译。有条件的编译意味着仅当特定的条件满足时,才对源代码块进行编译。在很多方面,预处理编译指令#if与if语句类似,区别在于,if语句控制特定的语句是否执行,而#if控制是否编译。

#if 语句块的结构如下:

#if condition_1

   Statement_block_1

#elif condition_2

Statement_block_2

。。。

#elif condition_3

Statement_block_3

#else

   Default_statement_block

#endif

    #if使用的测试表达式可以是结果为常量的任何表达式。不能使用sizeof()运算符、强制类型转换或float类型。#if最常用于检测#define编译指令创建的符号常量。

其中每个Statement_block由一条或多条语句组成,这些语句可以是任何类型的语句,包括预处理编译指令。无需使用花括号将他们括起,虽然也可以这样做。

注意:#if…#endif结构中的语句块最多有一个被编译。如果其中没有#else编译指令,则可能没有任何语句被编译。还可以使用#if…#endif 来帮助调试。

 

4.避免将头文件包含多次

   随着程序增大或使用头文件越来越频繁,很可能无意间包含一个头文件多次。这可能导致编译器感到迷惑,而停滞编译。

 

5.#undef编译指令

   #undef编译指令的功能与#define相反,它撤销对名称的定义。可以使用#undef和#define来创建只在源代码的某些部分被定义的名称。结合使用这种功能和#if编译指令,可以更好地控制条件编译。

 

二。编译阶段

编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。例如,C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。

       三。汇编阶段。接下来,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。如果我们在文本编辑器中打开hello.o文件,看到的将是一堆乱码。

      四。链接阶段。请注意,hello程序调用了printf函数,它是每个C编译器都会提供的标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值