作者:云东 原文:www.cnblogs.com/dongdongwei…
原文的排版不是很清晰,我对它进行了重新调整,并标注了重点
了解编译过程的益处
C++ 工程相关的问题
- 什么是库? 静态库和动态库又有什么区别?
- 头文件起什么作用?
编译过程简介
编译
把源文件中的源代码翻译成机器语言,保存到目标文件中。如果编译通过,就会把 cpp 转换成 obj 文件。
编译单元
每个 cpp 就是一个编译单元,每个编译单元相互之间是独立且相互不知的。一个编译单元(Translation Unit)是指一个 .cpp 文件以及这所 include 的所有 .h 文件,.h 文件里面的代码将会被扩展到包含它的 .cpp 文件里,然后编译器编译该 .cpp 文件为一个 .obj 文件,后者拥有 PE(Portable Executable,即 Windows 可执行文件)文件格式,并且本身包含的就是二进制代码,但是不一定能执行,因为并不能保证其中一定有 main 函数。当编译器将一个工程里的所有 .cpp 文件以分离的方式编译完毕后,再由链接器进行链接成为一个 .exe 或 .dll 文件。
目标文件
编译后生成的文件,以机器码的形式包含了编译单元里所有的函数和数据、导出符号表、未解决符号表、地址重定向表等。
目标文件的类型:
可重定位文件(.o、.obj文件)
其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。每个 cpp 会被编译成一个 .o 文件
共享的目标文件(库文件)
这种文件存放了适合于在两种上下文里链接的代码和数据。
- 第一种是静态链接程序(静态库)可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件。 静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码
- 第二种是动态链接程序(动态库)将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。 动态链接库在程序执行时才被调用。
可执行文件
一个可以被操作系统创建一个进程来执行之的文件。.o 文件在编译后就能获得,但是库文件、可执行文件都需要在链接后才能获得。
编译过程
C++ 程序编译过程图
作用
编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再转换为机器代码,生成目标文件(.obj)
分为两个过程
预处理阶段
- 宏
#define - 条件编译指令,如
#ifdef,#ifndef,#else,#elif,#endif等。 - 头文件包含,
#include <iostream> - 特殊符号,
LINE标识将被解释为当前行号(十进制数);FILE则被解释为当前被编译的 C 源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
编译、优化阶段
- 针对代码优化,不依赖具体计算机
- 针对计算机优化
汇编
把汇编语言代码翻译成目标机器指令,生成目标文件(.o文件、.obj文件)。此过程会依赖机器的硬件和操作系统环境。
三张表:.o 文件至少要提供三张表
1. 导出符号表
即该目标文件可以提供的符号及地址
2. 未解决符号表
即找不到地址的符号的列表,告诉链接器这些符号没找到地址
3. 地址重定向表
链接的时候,链接器会为目标文件的 “未解决符号表” 里的符号在其他目标文件中寻找地址,但是每个目标文件的地址都是从 0x0000 开始的,这样直接将对方文件中符号的地址拿过来用显然会是不正确的,为了区分不同的文件,链接器在链接时就会对每个目标文件的地址进行调整。在以下这个例子中,假如 B.obj 的 0x0000 被定位到可执行文件的 0x00001000 上,而 A.obj 的 0x0000 被定位到可执行文件的 0x00002000 上,那么实现上对链接器来说,A.obj 的导出符号的地址都会加上 0x00002000 ,B.obj 所有的符号地址也会加上 0x00001000 。这样就可以保证地址不会重复。因为被加上了起始地址,所以符号在自身文件中的实际地址就不对了,需要再用一张地址重定向表记录符号相对自身文件的地址
例子
链接过程
什么是链接
链接程序的主要工作就是将有关的目标文件(库文件、.o文件)彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
链接具体的工作
当链接器进行链接的时候,
- 首先,决定各个目标文件在最终可执行文件里的位置。
- 其次,访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。
- 再次,遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。
- 最后,把所有的目标文件的内容写在各自的位置上,再作一些另外的工作,就生成一个可执行文件。
链接方式
静态链接
函数的代码将从(其所在的)静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时,这些代码将被装入到该进程的虚拟地址空间中。
动态链接
函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。 链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
两种链接方式的比较
略
C/C++ 中提供的一些特性
extern
这就是告诉编译器,这个变量或函数在别的编译单元里定义了,也就是要把这个符号放到未解决符号表里面去(外部链接)。
static
- 如果该关键字位于全局函数或者变量的声明前面,表明该编译单元不导出这个函数或变量,因此这个符号不能在别的编译单元中使用(内部链接)。
- 如果是 static 局部变量,则该变量的存储方式和全局变量一样,但是仍然不导出符号。
默认链接属性
- 对于函数和变量,默认链接是外部链接
- 对于 const 变量,默认内部链接。
外部链接的利弊
外部链接的符号在整个程序范围内都是可以使用的,这就要求其他编译单元不能导出相同的符号(不然就会报 duplicated external symbols)。
为什么头文件里一般只可以有声明不能有定义
头文件可以被多个编译单元包含,如果头文件里面有定义的话,那么每个包含这头文件的编译单元都会对同一个符号进行定义,如果该符号为外部链接,则会导致 duplicated external symbols 链接错误。
为什么公共使用的 “内联函数” 要定义于头文件里
因为编译时编译单元之间互不知道,如果内联被定义于 .cpp 文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义,因此无法对函数进行展开(内联函数不展开,即不采用在使用处标记函数代码再跳转的方式,而是直接将代码嵌入)。所以如果内联函数定义于 .cpp 里,那么就只有这个 .cpp 文件能使用它。
.h 中的内联函数之所以可以被多个 .cpp 包含而不造成符号冲突,因为它会被直接嵌入到调用的地方,内部联结不形成外部符号, 对外不可见
常见编译器
略
makefile 及 make 工具
略
常见编译器使用方法
略
编译错误解析
略
Visual Studio 的 pch 文件
pch.h 是一个预编译的头文件,它其中包括任何稳定的头文件,如标准库头文件。你可以添加你想预编译的头文件。预编译头文件只在它或它所包含的任何文件被修改时才被编译。
参考:Microsoft 文档中预编译的头文件
初始的 pch.h 如下
#ifndef PCH_H
#define PCH_H
// TODO: add headers that you want to pre-compile here
#endif //PCH_H
pch.h 的好处
使用 pch.h 可以减少编译时间,特别是应用于大型头文件时,其中还包括许多其他头文件。
如何使用 pch.h
你可以如下添加你想使用的头文件到 pch.h
#ifndef PCH_H
#define PCH_H
#include <stdio.h>
#include <math.h>
#include "myheader.h"
#endif //PCH_H