1.2.3 按需编译:快速构建变更
假设需要修改主程序输出的字符串,修改后的程序如代码清单1.5所示。
代码清单1.5 ch001/按需编译/main.cpp
#include <iostream>
extern int fib25;
int main() {
std::cout << "The 25th item of Fibonacci Sequence is:" << fib25
<< std::endl;
return 0;
}
然后,重新构建该项目。等等!难道又是几十秒的等待吗?我们根本没有修改slow.cpp,其中计算出的斐波那契数列第25项数值并不会有任何改变。既然如此,为什么不复用上次编译的结果?
复用当然是可行的。程序构建过程并非只涉及编译,还有链接的过程。事实上,在运行编译器的时候,笔者会尽量采用最简单的方式,一步到位生成可执行文件,链接这一步就由编译器隐式地代劳了。
MSVC编译器在编译生成可执行文件的同时,在同一目录下还生成了一些.obj文件,这就是编译生成的目标文件。链接器的作用,就是把这些目标文件链接在一起,解析其中未定义的符号引用[1]。GCC等编译器其实也是一样的,只不过可能并没有将目标文件输出到工作目录中。
[1]符号一般指函数、变量、类等可被链接的对象的名称。
再回顾一下代码清单1.4,程序中声明了一个外部变量fib25。当编译器编译主程序main.cpp的时候,并不知道这个fib25的变量到底定义在哪里。因此,可以说对fib25的引用就是一个未定义的符号引用。这个未定义的符号引用也会存在于编译器生成的目标文件main.obj中。而编译器编译slow.cpp的时候,则会将fib25 的定义编译到目标文件slow.obj中。最后,链接器会将main.obj与slow.obj两个目标文件链接在一起,从而完成未定义符号fib25的解析。因此,按需编译的关键,就是分别编译各个源程序到目标文件。当源程序发生修改时,只需将变更的源程序重新编译到目标文件,然后重新与其他目标文件链接,如图1.2所示。
图1.2 按需编译示意图
使用MSVC按需构建
MSVC编译器的/c参数,可以使编译器仅将源程序编译为目标文件,而不进行链接过程。
首先,借助该参数将原始的main.cpp和slow.cpp编译好。当然,这一步骤仍然耗时:
> cd CMake-Book\src\漫长等待
> cl /c main.cpp slow.cpp /EHsc
> dir
main.cpp main.obj slow.cpp slow.obj
接着,尝试链接一下刚生成的两个目标文件,看看是否可以生成最终的可执行文件[2]:
> cl main.obj slow.obj
> main.exe
斐波那契数列第25项为:75025
[2]实际上,link.exe才是MSVC的链接器,但MSVC编译器cl.exe本身支持调用链接器,因此可以直接调用cl.exe来完成链接。
一切正常!下面修改主程序中的输出(实例中通过复制并覆盖来完成修改)并重新编译main.cpp到目标文件:
> copy /Y ..\按需编译\main.cpp main.cpp
> cl -c main.cpp /EHsc
如果读者正跟着我一起实践,应该感受到了main.cpp飞快的编译过程!最后,再次链接两个目标文件,验证我们的变更:
> cl main.obj slow.obj
> main.exe
The 25th item of Fibonacci Sequence is:75025
变更生效了,而且第二次编译也无须漫长的等待。
使用GCC按需构建
构建原理是相通的,因此不同编译器的构建过程也都是相似的,甚至用于编译为目标文件的参数都采用了字母c:
$ cd CMake-Book/src/漫长等待
$ g++ -c main.cpp slow.cpp
$ ls
main.cpp main.o slow.cpp slow.o
不同于Windows平台的.obj文件,Linux中的目标文件一般使用.o作为扩展名。
人生中宝贵的十几秒又消逝了,话不多说,同样修改(实例中通过复制并覆盖的方式修改)并重新编译main.cpp,然后重新链接并测试运行[3]:
$ cp -f ../按需编译/main.cpp ./main.cpp
$ g++ -c main.cpp
$ g++ main.o slow.o
$ ./a.out
The 25th item of Fibonacci Sequence is:75025
[3]GCC使用的是GNU链接器ld。类似于MSVC编译器,GCC本身也可以调用链接器,因此这里直接通过 GCC编译器完成链接过程。