C++程序的编译连接过程主要有预处理, 编译, 连接这几个阶段:
1 预处理:
预处理是在编译以前, 由编译器调用的一个独立程序, 即预编译处理器, 对源代码进行处理, 预处理主要负责如下工做:
1) 替换宏
2) 删除注释
3) 处理预处理指令, 如#include, #ifndef函数
源程序文件通过预处理后, 就造成一个包含全部必要信息的单个源文件, 一个源文件就是一个编译单元, 这个编译单元, 即源文件会被编译成同名的目标文件(.o或.obj)对象
2 编译:
编译过程负责如下工做:
1) 语法分析, 检查语法错误
2) 编译源单码, 即编译单元, 将编译单元中以文本形式存在的源代码编译成机器语言形式的目标文件
内联函数的替换也发生在这一阶段, 编译过程当中, 每一个编译单元是相互独立的, 即每一个源文件之间不知道对方的存在, 除了像include "xxx.cpp"这样极其错误的写法.
在目标文件中, 除了有数据和二进制代码, 还有至少3张表: 未解决符号表, 导出符号表, 地址重定向表.
未解决符号表记录全部在该编译单元中引用(好比使用其它源文件中的函数, 全局变量等)可是定义不在该编译单元中的符号及其在该编译单元中出现的地址, 在连接阶段, 连接器会从其它目标文件的导出符号表中查找该表中记录的符号, 若是该表中记录的符号在连接阶段未所有找到, 就会报"unresolved external link"这样的连接错误.
导出符号表记录该编译单元中定义的, 而且可以提供给其它编译单元使用的符号及其地址, 其它编译单元的未解决符号表中的记录的符号就须要从导出符号表中查找地址重定向表记录了该目标文件中全部对自身地址引用的记录, 这些记录实际上至关于在该目标文件中的地址偏移.编译器
3 连接:
连接过程是将全部目标文件连接到一块, 造成一个可执行文件.
连接器进行连接的时候, 首先决定各个目标文件在最终可执行文件里的位置, 而后访问全部目标文件的地址重定向表, 对其中记录的地址进行重定向(即加上该编译单元实际在可执行文件里的起始地址), 而后遍历全部目标文件的未解决符号表, 而且在全部的导出符号表中查找匹配的符号, 并在未解决符号表中所记录的位置上填写实际地址(即该符号在拥有其定义的目标文件中的实际地址),最后把全部目标文件的内容写在各自的位置, 就生成了可执行文件.io
内部连接与外部连接:
外部连接: 编译单元中, 若是一个名称在连接期能提供给其它编译单元使用, 能够和其它编译单元交互, 那么这个名称就有外部连接; 如下状况有外部连接:
1) 类的非inline函数, 包括静态和非静态成员函数
2) 类静态成员变量
3) 命名空间或全局的非静态自由函数, 非静态友元函数及非静态变量
内部连接: 编译单元中, 若是一个名称是局部的, 而且在连接时不会与其它编译单元中的一样名称相冲突, 那么这个名称就有内部连接; 如下状况有内部连接:
1) 全部的声明
2) 命名空间或全局的静态自由函数, 静态友元函数, 静态变量的定义
3) enum定义
4) inline函数定义(包括自由函数和非自由函数)
5) 类定义
6) const常量定义
7) union的定义
extern关键字告诉编译器, 这个符号在别的编译单元中定义, 也就是要把这个符号放到未解决符号表中, 也就是外部连接.
static关键字位于全局函数或变量声明的前面, 代表该编译单元不导出这个符号, 所以没法在别的编译单元里使用, 也就是内部连接.
自由函数和变量默认是外部连接, const默认是内部连接, 能够经过使用extern和static改变连接属性
常见问题
头文件里通常只能够有声明, 不能有定义, 由于头文件能够被多个编译单元包含, 若是头文件里有定义, 那么每一个包含该头文件的编译单元就都会同一个符号进行定义,若是该符号为外部连接, 就会出现重复定义的连接错误, 因此若是头文件若是要定义, 要么确保该头文件不会被多个编译单元引用, 要么确保定义的符号都具备内部连接.
const默认为内部连接是为了可以在头文件中定义常量, 例如const int n = 0; 因为常量是只读的, 因此即便每一个编译单元都有一份定义也没有关系, 不过有2种状况须要考虑:
1 若是涉及对这个const对象取地址而且依赖于这个地址的惟一性, 那么在不一样的编译单元里, 取到的地址不一样.
2 若是这个对象具备mutable属性, 某个编译单元对其进行修改, 其它编译单元看不到这一改变.
若是一个定义于头文件中的变量拥有内部连接, 那么若是有多个编译单元包含该头文件, 即有多个编译单元中都定义了该变量, 则其中一个编译单元对该变量进行修改, 其它编译单元中就看不到这一改变.
非静态函数和类的非inline函数默认是外部连接, 由于若是是内部连接, 可能会有人倾向于把定义写在头文件里, 这样的话一旦函数修改, 全部包含该头文件的编译单元都要从新编译.
不容许在类的定义中对类的静态成员就地初始化, 由于类声明一般在头文件, 这样就至关于在头文件中定义类的静态成员, 而静态成员具备外部连接, 若是该头文件被包含在多个编译单元中, 就会出现连接错误.
内联函数要定义于头文件里, 由于编译单元之间相互独立, 相互不知道, 若是内联函数定义于源文件中, 编译其它使用该函数的编译单元时没有办法找到函数定义, 所以也没法对函数展开.
若是定义于头文件里的内联函数被拒, 那么编译器会自动在每一个包含了该头文件的编译单元里定义这个函数而且不导出符号.编译