C/C++之应用程序的编译过程

一、C/C++语言由源代码生成的各阶段如下

源程序->编译预处理->编译->优化程序->汇编程序->链接程序->可执行文件

其中编译预处理阶段,读取源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。或者说是扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。预处理过程先于编译器对源代码进行处理。下面使用VC编译为例,部分内容在gcc中不支持

预处理阶段

尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。

一、伪指令(或预处理指令)定义

预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。

二、预处理指令主要包括以下四个方面:

1、宏定义指令#define

宏定义了一个代表特定内容的标识符。预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。宏定义最常见的用法是定义代表某个值的全局符号。宏的第二种用法是定义带参数的宏(宏函数),这样的宏可以像函数一样被调用,但它是在调用语句处展开宏,并用调用时的实际参数来代替定义中的形式参数。

用法一:

#define PI 3.1415926

注意:

(1)作为一种约定,习惯上总是全部用大写字母来定义宏,这样易于把程序的宏标识符和一般变量标识符区别开来。使用宏的好处有:

一是使用方便。

二是定义的宏有了意义,可读性强。

三是容易修改。

(2)宏表示的值可以是一个常量表达式,允许宏嵌套(必须在前面已定义)。例如:

#define ONE 1

#define TWO 2

#define SUM (ONE+TWO)

这里需要注意括号的使用,尽管它们并不是必须的。但出于谨慎考虑,还是应该加上括号的。预处理仅是简单的字符替换,并不会处理优先级。

(3)宏还可以代表一个字符串常量,例如:

#define VERSION "Version 1.0"

(4)带参数的#define指令(宏函数)

带参数的宏和函数调用看起来有些相似。看一个例子:

#define SUM(x,y) (x+y)

可以时任何数字表达式甚至函数调用来代替参数x、y。这里再次提醒大家注意括号的使用。宏展开后完全包含在一对括号中,而且参数也包含在括号中,这样就保证了宏和参数的完整性。看一个用法:

sum = SUM(2, 3); 展开后变为 sum = (2 + 2);

1.2 #运算符

出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。有时把这种用法的#称为字符串化运算符。例如:

宏定义中的#运算符告诉预处理程序,把源代码中任何传递给该宏的参数转换成一个字符串。所以输出应该是12345。

1.3 ##运算符(很少用)

##运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。看下面的例子:

2、条件编译指令。

程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。条件编译指令将决定那些代码被编译,而哪些是不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。这些指令包括:#if/#ifdef/#ifndef/#else/#elif/#endif

#if指令检测跟在制造另关键字后的常量表达式。如果表达式为真,则编译后面的代码,直到出现#else、#elif或#endif为止;否则就不编译。

#endif用于终止#if预处理指令。

#else指令用于某个#if指令之后,当前面的#if指令的条件不为真时,就编译#else后面的代码。

#elif预处理指令综合了#else和#if指令的作用。

#ifdef和#ifndef这二者主要用于防止重复包含。我们一般在.h头文件前面加上这么一段:

#ifndef FUNCA_H

#define FUNCA_H

    //头文件内容

#endif

这样,如果a.h包含了funcA.h,b.h包含了a.h、funcA.h,重复包含,会出现一些type redefination之类的错误。

3、特殊符号。

预编译程序可以识别一些特殊的符号。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

__FILE__ 包含当前程序文件名的字符串

__LINE__ 表示当前行号的整数

__DATE__ 包含当前日期的字符串

__STDC__ 如果编译器遵循ANSI C标准,它就是个非零值

__TIME__ 包含当前时间的字符串

注意:是双下划线,而不是单下划线 。

#error指令将使编译器显示一条错误信息,然后停止编译。

#line指令改变_LINE_与_FILE_的内容,它们是在编译程序中预先定义的标识符。

#pragma指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。

4、头文件包含指令。

这是最常见的。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。

#include预处理指令的作用是在指令处展开被包含的文件。包含可以是多重的,也就是说一个被包含的文件中还可以包含其他文件。标准C编译器至少支持八重嵌套包含。预处理过程不检查在转换单元中是否已经包含了某个文件并阻止对它的多次包含,这个的处理办法使用上面给出的条件预处理指令。

include文件的展开是一个很简单的过程,只是将include文件包含的代码拷贝到包含当前cpp文件中。

(1)没有被任何的其它cpp文件或者头文件包含的.h文件将不会被编译。也不会最终成为应用程序的一部分。

编译C++工程后你会发现,并没有报告上面的代码错误。这说明.h文件本身不是一个编译单元。只有通过include语句最终包括到了一个.cpp文件中后才会成为一个编译单元。

(2)存在一种可能性,即一个cpp文件直接的或者间接的包括了多次同一个.h文件。

上面这样的多重包含 就出现编译错误

(3)include文件是按照定义顺序被展开到cpp文件中的

编译和链接。

C++的编译实际上分为编译和链接两个阶段,这两个阶段联系紧密。根据C++标准,一个编译单元(Translation Unit)是指一个.cpp文件以及它所include的所有.h文件,.h文件里面的代码将会被扩展到.cpp文件里,然后编译器编译该.cpp文件生成一个.obj文件。obj文件拥有PE[Portable Executable,即windows可执行文件]文件格式,并且本身包含的就已经是二进制码,但是,不一定能够执行。当编译器将一个工程里的所有.cpp文件都编译完毕后,再由链接器进行链接,成为一个.exe或库文件。

编译上面的项目,VS会生成如下文件

生成的目标文件为可重定位文件(Relocatable File)

这里有个问题,虽然test.h对main.cpp是可见的(main.cpp包含了test.h),但是test.cpp对main.cpp并不可见,那么main.cpp是如何找到func函数的实现的呢?

实际上,在单独编译main.cpp文件的时候编译器并不先去关注func函数是否已经实现,或者在哪里实现。它只是把它看作一个外部的链接类型,认为func函数的实现应该在另外的一个obj文件中。在调用func的时候,编译器仅仅使用了一个地址跳转,但是由于并不知道foo具体存在于哪个地方,因此只是在jump后面填入了一个假的地址。然后就继续编译下面的代码。当所有的cpp文件都执行完了之后就进入链接阶段。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/zcshoucsdn/article/details/79334647