一、前言
C/C++的编译过程包含了四个步骤:
1. 预处理(Preprocessing)
2. 编译(Compilation)
3. 汇编(Assemble)
4..链接(Linking)
二、预处理(也称为预编译)
预处理阶段主要处理一些预处理指令,比如文件包括、宏定义、条件编译。
1.文件包括
文件包括就是将所有的#include头文件替换成真正的内容。
格式:
#include "文件名"
或
#include <文件名>
2.宏定义
预处理时需要把所有的宏定义替换成真正的内容。
(1)宏定义的基本语法
#define 标识符 字符串
(2)示例
#define PI 3.1415926 //定义一个常量
#define MULTIPLY(x, y) ((x) * (y)) //定义一个乘法操作,宏定义需要注意()
#define foo(x) {x += 1; x *= 2} //定义一个类似函数的宏,注意有分号必须加上括号
其中要加上()的原因就是当调用MILTIPLY(a+b,a-b)时不加(),会变成a+b*a-b,导致程序出错。
要加上{}的原因就是当调用 if(x>0) foo(x);时, 程序会变成:
if(x>0)
x+=1;
x *= 2;
这时,程序不会按预期执行。
(3) 预定义宏
在C/C++中有一些预定义的宏,可以拿来直接用,如:
__FUNTION__ 获取当前函数名
__LINE__ 获取当前代码行号
__FILE__ 获取当前文件名
__DATE__ 获取当前日期
__TIME__ 获取当前时间
__STDC_VERSION__ 获取当前编译器的版本
(4)优点
i. 方便程序的修改,如用宏定义定义常量,既可达到修改一处,多处改变的效果。
ii. 提高程序的运行效率,比如通过使用带参数的宏定义可以完成函数调用的功能,而又避免可函数调用带来的开销(如保留调用函数的现场和恢复调用函数的现场所带来的开销)。
(5)缺点
i. 宏定义是通过在预处理时直接替换的,并不会检查参数是否合法,存在安全隐患。
ii. 嵌套宏定义过多可能会印象程序的可读性,很容易出错,不容易调试。
(6) 宏常量和const常量的区别
i. const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
ii. 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。规则5-2-1:在C++ 程序中只使用const 常量而不使用宏常量,即const 常量完全取代宏常量。
(7) 带参数宏定义和内联函数的区别
i. 同样的,内联函数有数据类型,可以检查参数是否合法,而带参数宏定义不会
ii. 带参数宏定义是在预处理阶段展开的,而内联函数实在编译阶段展开的,两种都不会带来函数调用的开销。
3.条件编译
在预处理阶段,通过条件编译指令决定哪个程序段需要编译,那些不需要编译。为后面的编译工作做铺垫。
(1)格式
i. 当标识符已经定义时,程序段1才参加编译。
#ifdef 标识符
程序段1
#else
程序段2
#endif
ii. 当标识符没有被定义,则重定义标识1,且执行程序段1。
#ifndef 标识符
#define 标识1
程序段1
#endif
iii. 当表达式1成立时,编译程序段1, 后面以此类推,与条件语句一样。
#if 表达式1
程序段1
#elif 表达式2
程序段2
……
#elif 表达式n
程序段n
#else
程序段n+1
#endif
(2) 作用
ii. 预编译使问题或算法的解决方案增多,有助于我们选择合适的解决方案。
iii. 通常我们在头文件中加入以下内容,通过选择判断来避免编译阶段产生重复编译。
#ifndef _A_H_
#define _A_H_
//content of head file of A
#endif
在一个工程里,一个 .cpp文件包含了 a.h头文件和b.h头文件,但其实b.h中又通过include包含了a.h头文件,所以在编译过程.cpp文件会对a.h进行两次编译(编译过程是把a.h中的声明和定义变成汇编代码,一般没有定义), 这带来的影响一个是如果a.h的代码量很大,那么就会浪费大量的时间。另外,更为严重的是如果a.h头文件中含有变量或函数的定义,由于重复编译会导致重复定义,使程序在编译链接的时候崩溃。(注:声明和定义的区别详见:C语言中的声明和定义,总结一句话就是声明不分配内存,只是表明有该变量或函数,定义分配内存,实质上产生该变量或函数)。所以我们一般不会在头文件中进行定义。再回来,所以我们解决第一个影响就是通过#Ifndef的方式避免重复编译,同时避免有可能产生的重复定义。
重复定义还有其他情况:详见:C++:重复编译与重复定义
在Microsoft studio 中通过在头文件中加入#pragma once,可以达到同样的效果。
二、编译
编译阶段进行语法分析、词法分析和语义分析,并且将代码优化后产生相应的汇编代码文件(ASCLL文件),即.s 文件。这个过程是整个程序构建的核心部分,也是最复杂的部分之一。
三、汇编
通过不同平台(Windows、Linux)的汇编器将汇编代码翻译成机器码,即生成二进制可重定向文件(.o)。
任何一个源文件在进行编译阶段的时候会去产生符号表,符号表中存放的就是程序所产生的符号(例如:函数名,变量名等),我们的编译阶段是不会去给符号分配正确的地址。这些符号都没有被分配地址,因此.o文件没有经过链接是无法执行的。
四、链接
1. 程序的链接阶段可分为两个步骤:
(1)第一步:由于每个.o文件都有都有自己的代码段、bss段,堆,栈等,所以链接器首先将多个.o 文件相应的段进行合并,建立映射关系并且去合并符号表。进行符号解析,符号解析完成后就是给符号分配虚拟地址。
(2)第二步:将分配好的虚拟地址与符号表中的定义的符号一一对应起来,使其成为正确的地址,使代码段的指令可以根据符号的地址执行相应的操作,最后由链接器生成可执行文件。
2. 链接的两种方式:动态链接和静态链接
(1)静态链接:
要了解静态链接,我们得先了解静态库,静态库(static library)是“库”最典型的使用方式。在UNIX系统中,一般使用 ar 命令生成静态库,并以 .a 作为文件扩展名,”lib” 作为文件名前缀。在Windows平台上,静态库的扩展名为 .LIB。链接器在将所有目标文件集链接到一起的过程中,会为所有当前未解决的符号构建一张“未解决符号表”。当所有显示指定的目标文件都处理完毕时,链接器将到“库”中去寻找“未解决符号表”中剩余的符号。如果未解决的符号在库里其中一个目标文件中定义,那么这个文件将加入链接过程,这跟用户通过命令行显示指定所需目标文件的效果是一样的,然后链接器继续工作直至结束。
总的来说,静态链接就是在链接阶段把.o文件中所依赖的静态库链接到一起,最终生成的可执行文件当中包含lib中的函数,类等等。
(2)动态链接
相对应的,动态链接所对应的库叫做动态链接库(Dynamic Linkable Library,缩写为DLL)。
对于像 C 标准库这类常用库而言,如果用静态库来实现存在一个明显的缺点,即所有可执行程序对同一段代码都有一份拷贝。如果每个可执行文件中都存有一份如 printf, fopen 这类常用函数的拷贝,那将占用相当大的一部分硬盘空间,这完全没有必要。所以我们使用动态链接的方法来进行优化。
它是这样进行链接的,当链接器发现某个符号的定义在DLL中,那么它不会把这个符号的定义加入到最终生成的可执行文件中,而是将该符号与其对应的库名称记录下来(保存在可执行文件中)。当程序开始运行时,操作系统会及时地将剩余的链接工作做完以保证程序的正常运行。在 main 函数开始之前,有一个小型的链接器(链接器隶属于系统)将负责检查贴过标签的内容,并完成链接的最后一个步骤:导入库里的代码,并将所有符号都关联在一起。在系统的管理下,应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,系统才转去执行DLL中相应的函数代码。一般情况下,如果一个应用程序使用了动态链接库,Win32系统保证内存中只有DLL的一份复制品。
(3)两者的比较
i. 动态链接库的优点:(1)更加节省内存;(2)DLL文件与EXE文件独立,只要输出接口不变,更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性。
ii. 动态链接库的缺点: 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。
iii. 静态链接库的优点: (1) 代码装载速度快,执行速度略比动态链接库快; (2) 只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱等问题。
iv. 静态链接库的缺点: 使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费。
五、参考文献
【1】https://blog.csdn.net/u012248972/article/details/78823165
【2】https://blog.csdn.net/left_la/article/details/12098545
【3】https://baike.baidu.com/item/%E9%A2%84%E5%A4%84%E7%90%86%E5%91%BD%E4%BB%A4/10204389?fr=aladdin