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

📅 发布时间:2026/9/1 17:25:07
C/C++:编译全过程——预处理、编译、汇编、链接(包含预处理指令:宏定义,文件包括、条件编译) 一、前言C/C的编译过程包含了四个步骤1. 预处理()2. 编译()3. 汇编()4..链接()二、预处理也称为预编译预处理阶段, 主要是对一些预处理指令加以处理, 这些指令涵盖文件包括这一指令, 还有宏定义这一指令, 另外也包含条件编译这一指令。1.文件包括文件包括就是将所有的#头文件替换成真正的内容。格式# 文件名#2.宏定义预处理时需要把所有的宏定义替换成真正的内容。(1)宏定义的基本语法# 标识符 字符串(2)示例#define PI 3.1415926 //定义一个常量 #define MULTIPLY(x, y) ((x) * (y)) //定义一个乘法操作宏定义需要注意 #define foo(x) {x 1; x * 2} //定义一个类似函数的宏注意有分号必须加上括号那里边, 要添加的缘故是, 在调用a b, a - b时辰, 要是不加上, 就会变为a b * a - b, 进而致使程序出错。要加上{}的缘由在于, 当进行if(x0) foo(x);此调用操作的时候, 程序会转变成为:。if(x0) x1; 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. 同样的, 内联函数具备数据类型, 能够对参数是否合法进行检查, 然而带参数宏定义却不行。二、带有参数呈现的宏定义, 是于预处理这一阶段去展开的, 而充当内联功能的函数, 则是在编译过程所处的那一个阶段来将其进行展开的, 两种类型都不会为实际出现的函数调用导致任何耗损现象产生。3.条件编译于预处理时期, 借着条件编译指令来判定哪一个程序段是应该进行编译的, 哪些又是不需要进行编译的, 以此为后续的编译工作打造基础铺垫。(1)格式i. 当标识符已经定义时程序段1才参加编译。#ifdef 标识符 程序段1 #else 程序段2 #endif其二.要是标识符未曾被定义, 那么就重新定义标识1, 并且去执行程序段1。#ifndef 标识符 #define 标识1 程序段1 #endif三、当那个表达式一成立之际, 编译程序段一, 之后跟随着这样以此类推去进行, 跟条件语句是一样的情形。#if 表达式1 程序段1 #elif 表达式2 程序段2 …… #elif 表达式n 程序段n #else 程序段n1 #endif(2) 作用i. 使用条件编译可以使目标程序变小运行时间变短。ii. 预编译致使问题或者算法的解决办法数量增加, 这对我们挑选适合的解决办法存在着帮助。通常, 我们会将以下内容加入到头文件里, 借助选择判断, 以此来防止编译阶段制造出重复编译情况出现。#ifndef _A_H_ #define _A_H_ //content of head file of A #endif有一个工程, 其中一个.cpp文件, 它包含了a.h头文件, 还包含了b.h头文件, 然而b.h, 它又通过某种方式包含了a.h头文件, 如此一来在编译这个过程中, .cpp文件会对a.h进行两次编译, 所谓编译过程, 就是把a.h里的声明以及定义转化成汇编代码, 通常情况下是没有定义的, , 对了, 讲这么多就是想说这么一变, 带来的影响就是, 要是a.h的代码量特别大, 那就会浪费掉大量的时间。除了这个之外, 更为严重的情况是, 要是a.h头文件当中包含有变量或者函数的定义, 因为重复编译的缘由会致使重复定义出现, 进而让程序在编译链接这个阶段的时候崩溃。需要注意的是: 声明和定义之间的区别详细说明可见于: C语言里的声明和定义, 总结成一句话来讲就是声明不会分配内存, 仅仅是表明存在该变量或者函数, 而定义是会分配内存的, 实质意义上就是产生该变量或者函数。所以我们一般而言是不会在头文件当中进行定义的。再转回到刚才的话题, 所以我们解决第一个影响的办法就是采用#的方式来避免重复编译, 与此同时避免有可能会产生的重复定义。重复定义还有其他情况详见C重复编译与重复定义通过在头文件里加入# once, 能够在其中达致同样的成效, 在 中。二、编译语法分析于此被执行以开展编译阶段, 执行词法分析也恰在编译阶段之时, 语义分析同样于编译阶段进行着, 经过代码优化之后产生对应的汇编代码文件, 也就是ASCLL文件, 即.s文件。这一过程是塑造整个程序的核心部分, 它也是极为复杂的部分当中的一个。三、汇编借助不同平台其中包含Linux之上的汇编器, 把汇编代码转变为机器码, 亦即是要生成二进制可重定向文件.o。在编译阶段, 任何一个源文件会去产生符号表符号表所存放的是程序产生的符号, 像函数名、变量名等, 编译阶段不会给符号分配正确地址, 这些符号都没被分配地址, 所以.o文件没经链接无法执行。四、链接 1. 程序的链接阶段可分为两个步骤(1)首先是第一步, 因为每个.o 文件都存在各自的代码段、bss 段, 还有堆以及栈等等, 所以链接器会先将好几个.o 文件对应的段予以合并, 建起映射关系, 并且去合并那符号表弄进行符号解析。当符号解析完毕之后, 接下来就是给那些符号分配虚拟地址了。(2)先是第二步, 要把分配好的虚拟地址, 跟符号表中定义的符号逐一对应起来, 以此形成正确的地址, 方能让代码段的指令, 依据符号的地址去执行对应的操作, 最终由链接器生成可执行文件。2. 链接的两种方式动态链接和静态链接(1)静态链接若想知晓静态链接, 那我们就得先去了解静态库, 静态库, 它是“库”极为典型的运用方式。于UNIX系统里, 通常会运用ar命令去生成静态库, 并且是以.a作为文件扩展名, 而“lib”则作为文件名前缀。在该平台上, 静态库的扩展名是.LIB。链接器于把所有目标文件集链接到一块儿的进程中, 会为所有当下尚未解决的符号构建一张“未解决符号表”。当所有显示明确指出的目标文件都已处理完成的时候, 链接器会前往“库”那里去寻觅“未解决符号表”里剩余的符号, 要是未解决的符号在库里某一个目标文件当中被定义, 那么这个文件将会被加入到链接进程里就如同用户借助命令行显示指定所需目标文件所产生的效果是一样的, 接着链接器持续开展工作一直到结束。归纳来讲, 静态链接是于链接阶段, 将.o文件里所依赖的静态库予以链接整合, 最终生成的可执行文件之中涵盖lib里的函数、类等内容。(2)动态链接相对而论的是, 与动态链接相互对应的那个库, 被称作动态链接库, 其缩写为DLL。就像 C 标准库这类通常被使用的库里而言情况如果是用静态库来达成的话那么具有这样一个非常清晰的不足也就是全部的用来执行的程序对着相同一段程序代码都有一份复制若是每一个用于执行的文件内容里都存在着像 fopen 这类常常会被用到的函数的拷贝这必定要去占用数目挺可观的一部分磁盘存储空间可这不具备必要性所以我们采用动态链接的办法来实施优化并且这是可行的。它是如此这般来进行链接的, 是在链接器察觉到某个符号的定义处于DLL之日, 那么它会并非将这个符号的定义增添至最终所生成的可执行文档当中的情况, 而是把该符号跟其相对应的库名称记录下来并且是保存在可执行文件里如此这般。在程序开启运行的时候, 操作系统会赶忙把剩余的链接活儿干完, 以此来确保程序能够正常运行。在main函数起始之前, 有个隶属系统的小型链接器, 其会负责检查已贴标签的内容, 去完成链接的最终步骤, 也就是导入库里的代码, 还要把所有符号关联到一起。在系统这般管理之下, 应用程序与对应的DLL之间建立起链接关系。当要执行所调用DLL里的函数时, 依据链接产生的重定位信息, 系统这才转去执行DLL中相应的函数代码。一般状况下, 要是有一个应用程序运用了动态链接库, Win32系统会确保在内存里仅有DLL的一份重复复刻品。(3)两者的比较i. 对于动态链接库而言, 其具备这样一些优点: 其一, 它能够更加节省内存其二, DLL文件和EXE文件是相互独立的, 并且只要输出接口保持不变, 那么更换DLL文件并不会对EXE文件产生任何的影响, 如此一来, 便能够极为有力地提高可维护性以及可扩展性。ii. 动态链接库存在的缺点: 使用动态链接库的应用程序并非自完备, 其所依赖的DLL模块同样需要存在, 倘若使用载入时动态链接, 程序启动之际若发现DLL不存在, 系统将会终止相应程序并给出错误提示信息。iii. 静态链接库所具备的优点: (1) 代码进行装载期间的速度较为快速, 其二执行的速度相较于动态链接库而言略微要快一些 (2) 仅仅只需要确保在开发者所使用的计算机当中存在正确的.LIB文件, 当以二进制形式去发布程序的时候并不需要去考虑在用户的计算机之上.LIB文件是否是存在的以及是否存在版本方面的问题, 这样能够避免诸如DLL地狱等之类的问题。4. 静态链接库存在这样的缺点, 使用静态链接生成的可执行文件, 其体积是比较大的, 还包含着相同的公共代码, 这就导致了浪费的情况出现。