C/C++:编译全过程——预处理、编译、汇编、链接(包含预处理指令:宏定义,文件包括、条件编译)

一、前言

    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) 作用

     i. 使用条件编译可以使目标程序变小,运行时间变短。

     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

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值