C++ 编译过程
在介绍编译器以前,先简单地说一下 C++ 的编译过程,以便理解编译器的工做。
编译(compiling)并不意味着只建立仅仅一个可执行文件。建立一个可执行文件是一个多级过程,其中最重要的过程是预处理(preprocessing),编译(compliation)和连接(linking)。从源代码文件到一个可执行文件的整个过程,最好的说法是 build(中文翻译的话,有叫生成,有叫编译连接,也有叫构建)。compiling 仅仅是 build 过程的一部分,但你常常会碰到许多人把 compile 指代整个过程。一般状况下,你不须要为这几个过程运行单独的命令,编译器本身会调用,如预处理器。ios
2.1 预处理
build 过程的第一步就是编译器运行 C 预处理器,目的是对代码文件进行文本上的处理。它会处理头文件包含指令(#include),条件编译指令(#ifdef……#endif)和宏(#define),这些指令叫作预处理指令,都以井字符 # 开头。编译器自己是绝对看不到这些预处理指令的。
好比:函数
#include <iostream>
这句代码会告诉预处理指令,要把 iostream 的文件内容抓去到当前文件,你每包含一个头文件,它就会把这个头文件的内容粘贴到这个文件中,而后把 #include 指令移除。测试
#define MY_NAME "Alex"
宏就是一个被其它内容(可能比较复杂)替换掉的字符串内容,此时预处理器会把下面的代码:ui
cout << "Hello" << MY_NAME << endl;
展开成:spa
cout << "Hello" << "Alex" << endl;
因为预处理器在编译器以前处理代码,它也能够用来移除代码——有时,你会要在代码里执行某些测试代码。你能够告诉预处理器,若是定义了某个宏,则包含某些代码。而后,若是你想执行这个代码,就定义这个宏,不然就移除掉这个宏的定义。翻译
#include <iostream> #define DEBUG using namespace std; int main() { int x; int y; cout << "Enter value for x: "; cin >> x; cout << "Enter value for y: "; cin >> y; x *= y; #ifdef DEBUG cout << "x: " << x << '\n' << "y: "<< y; #endif }
若是你不想执行变量的打印,那么只需简单注释掉 #define DEBUG 就行。
一样地,你也能够用 #ifndef 来改变条件——若是没有定义……这个方法一般用在引入多个头文件的时候。code
2.2 编译
编译意味着把一个源文件(.cpp)转变成一个对象文件(object,.o 或 .obj)。
一个对象文件会把你程序里的每个函数,封装成一个计算机处理器能理解的形式——机器指令(machine language instructions)。每个源文件都是单独编译过的,即对象文件包含的机器代码都是编译过的源代码。好比,你有三个源文件,通过编译,生成了三个对象文件,每个对象文件都包含了各自对应的机器代码。
但你还不能运行它们,这时候,就须要连接器了。对象
2.3 连接
连接(Linking),是把一堆对象文件和库(有时也可能仅仅是一个对象文件,但也须要连接)建立成一个单独的可执行文件(好比 .exe 或 .dll)。
连接器经过一种适当的格式建立一个可执行的文件,并传递每一个独立的对象文件内容到一个可执行的结果。连接器也处理含有对象文件源代码以外的其它函数的引用,好比 C++ 标准库里的函数。当你调用了一个 C++ 标准库的函数,如 cout << “Hi”,你就在使用一个本身代码中没有定义的函数,它被定义在一个相关的对象文件中,但这是由编译器提供的,并不属于你。在编译时,编译器知道这个函数是有效的,由于你引出了 iostream 头文件,但因为这个函数不是 cpp 文件的一部分,编译器就会在调用树(call tree)留下一个存根(stub),连接器会遍历对象文件,针对每个存根,它会找到正确的函数地址,而后从已连接过的其它对象文件中,用正确的地址替换掉对应的存根。
这个过程有时也叫作修正(fixup)。当你把你的程序分离成多个源文件时,你就会利用连接器来修正全部在源文件中调用过的函数。若是连接器找不到这个函数的位置,它就会生成一个 undefined function error,即使代码被编译器经过了,也不意味着代码是正确的。连接器是首先以全局的视角来探测这种错误的。blog