购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.2 构建多源程序

1.2.1 输出另一源程序的字符串

本例由1.1节的“您好,世界”程序修改而成。新建源文件msg.c,在其中定义输出的字符串内容,如代码清单1.2所示。

代码清单1.2 ch001/多源程序/msg.c
const char *msg = "您好,我来自msg.c!";

然后将“您好,世界”主程序中输出的内容修改为变量msg的值,同时将msg声明为一个外部变量,如代码清单1.3所示。

代码清单1.3 ch001/多源程序/main.c
#include <stdio.h>
 
extern const char *msg;
 
int main() {
    printf("%s\n", msg);
    return 0;
}

至此,本例的两个源程序就已经编写好了,那么该如何构建它们呢?既然只是多了一个源文件,想必直接罗列在编译器命令行参数的后面就可以了吧!

MSVC的构建过程如下:

> cd CMake-Book\src\ch001\多源程序
> cl main.c msg.c
> main.exe
您好,我来自msg.c!

GCC的构建过程如下:

$ cd CMake-Book/src/ch001/多源程序
$ gcc main.c msg.c
$ ./a.out
您好,我来自msg.c!

果然如此简单,甚至二者的命令行参数都毫无分别!接下来,我们让情况变得复杂一些。

1.2.2 一个需要漫长编译过程的程序

当程序体量逐渐变得庞大,编译时间也会越来越长。C++更是经常因为编译速度慢而被大家诟病。因此,我们在工程上常采取很多手段优化编译时间。其中,最简单直接的手段就是避免不必要的编译。简言之,当多次编译多份源文件时,编译器应当聪明地只把修改过的源程序重新编译,而复用其他未修改的已经编译好的程序。

为了演示这一策略的有效性,我们需要一个非常耗时的编译过程用于对比。当然,读者肯定不会乐意在第1章就接触一个庞大的工程案例。因此,本书在这里采用了C++“黑魔法”,也就是一段糟糕的模板编程来模拟需要长时间编译的源程序。本书不会讲解该程序细节,相关实现细节感兴趣的读者可自行参考本书配套资源。

本例将会输出斐波那契数列第25项的值。主程序main.cpp如代码清单1.4所示。

代码清单1.4 ch001/漫长等待/main.cpp
#include <iostream>
 
extern int fib25;
 
int main() {
    std::cout << "斐波那契数列第25项为:" << fib25 << std::endl;
    return 0;
}

主程序中声明的外部变量fib25被定义在另一个源程序slow.cpp中。这个源程序就是模拟长耗时编译的源程序,由于有“黑魔法”的存在,我们暂且不去关心它的写法。

使用MSVC构建本例

> cd CMake-Book\src\ch001\漫长等待
> cl main.cpp slow.cpp /EHsc
> .\main.exe
斐波那契数列第25项为:75025

MSVC编译器的/EHsc参数用于启用C++异常处理的展开语义,如果不指定会产生警告。其具体用途请参考其官方文档。

这个编译过程在笔者的移动工作站上运行了超过20秒!

使用GCC构建本例

GCC编译器编译该项目也需要十几秒的时间,过程如下:

$ cd CMake-Book/src/ch001/漫长等待
$ g++ main.cpp slow.cpp
$ ./a.out
斐波那契数列第25项为:75025

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文件,这就是编译生成的目标文件。链接器的作用,就是把这些目标文件链接在一起,解析其中未定义的符号引用 。GCC等编译器其实也是一样的,只不过可能并没有将目标文件输出到工作目录中。

再回顾一下代码清单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

接着,尝试链接一下刚生成的两个目标文件,看看是否可以生成最终的可执行文件

> cl main.obj slow.obj
> main.exe
斐波那契数列第25项为:75025

一切正常!下面修改主程序中的输出(实例中通过复制并覆盖来完成修改)并重新编译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,然后重新链接并测试运行

$ 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

1.2.4 使用Makefile简化构建

尽管我们在1.2.3节中掌握了如何避免不必要的编译,但真到了实践中却会发现很难操作。前面的实例还仅仅只有两个源程序,我们尚且能够判断修改了哪一个源程序。如果源程序更多,该怎么办呢?

GNU make(简称make)是Linux中一个常见的构建工具。Windows上也有类似的工具,称为NMake,语法与make不尽相同,也并不常用,因为大家往往更倾向于直接使用与Visual Studio集成度更高、功能更强大的MSBuild构建工具。那么不妨先重点看一下make的用法。

使用make工具

make构建工具会根据Makefile规则文件来进行构建。简言之,Makefile规则文件是由一系列面向目标的规则构成的。用于1.2.3小节实例的Makefile如代码清单1.6所示,注意Makefile中的缩进必须使用制表符(Tab键)而非空格。

代码清单1.6 ch001/Makefile/Makefile
main: main.o slow.o
    g++ -o main main.o slow.o
 
main.o: main.cpp
    g++ -c main.cpp -o main.o
 
slow.o: slow.cpp
    g++ -c slow.cpp -o slow.o

其中,冒号前面的是构建目标,冒号后面的是依赖目标。这里会建立构建目标对依赖目标的依赖关系,make能够据此安排好各个目标构建的次序。每个规则下面缩进的部分,就是构建这一目标所需要执行的命令。

另外,Makefile中对GCC/G++编译器指定的-o参数可以指定生成的目标文件的名称。

下面先使用make编译全部目标,第一次构建需要花费较长时间:

$ cd CMake-Book/src/ch001/Makefile
$ cp ../漫长等待/*.cpp ./
$ make
g++ -c main.cpp -o main.o
g++ -c slow.cpp -o slow.o
g++ -o main main.o slow.o
$ ./main
斐波那契数列第25项为:75025

make会将当前工作目录中的名为Makefile的文件作为默认规则文件。因此,这里调用make时不必指定 Makefile的文件名。如果需要指定自定义的Makefile文件名,可以使用-f参数。

修改主程序main.cpp(通过复制并覆盖文件的方式),再次调用make命令构建项目,可以看到 make只按需编译了变更的main.cpp:

$ cp -f ../按需编译/main.cpp
$ make
g++ -c main.cpp -o main.o
g++ -o main main.o slow.o
$ ./main
The 25th item of Fibonacci Sequence is:75025

第三次构建,make没有做任何操作,并友好地提示我们main已经是最新的了:

$ make
make: 'main' is up to date.

如果想使用Clang编译器而非GCC,也可以在调用make的时候加上参数:

$ make CXX=clang++

那么,make到底是怎么知道哪些源程序做了修改的呢?其实这里没有使用什么奇技淫巧,它只是简单地对比了一下构建目标与依赖目标的修改日期。但凡有一条构建规则中的依赖目标比构建目标更新,这一规则对应的命令就会被重新执行。

使用NMake工具

接下来简要介绍一下Windows平台中NMake的使用。首先是规则文件Makefile的书写方式,如代码清单1.7所示。

代码清单1.7 ch001/Makefile/NMakefile
main.exe: main.obj slow.obj
    cl /Fe"main.exe" main.obj slow.obj
 
main.obj: main.cpp
    cl /c main.cpp /Fo"main.obj" /EHsc
 
slow.obj: slow.cpp
    cl /c slow.cpp /Fo"slow.obj"

其与前Makefile最直观的不同还是编译器参数不同:

● /Fe指生成可执行文件的名称;

● /Fo指生成目标文件的名称;

● MSVC的目标文件扩展名一般为.obj而不是.o。

另外在后面的示例代码中,为了与make的Makefile做区分,NMake的Makefile文件均命名为NMakefile。由于NMake同样会将Makefile作为默认文件名,这里需要使用/F参数指定自定义文件名为NMakefile。

首先,使用NMake第一次构建项目,这同样需要漫长的等待:

> cd CMake-Book\src\ch001\Makefile
> copy ..\漫长等待\*.cpp .\
> nmake /F NMakefile
> main.exe
斐波那契数列第25项为:75025

然后,修改主程序后再次构建项目。注意,这里没有使用copy命令来修改源文件,因为Windows中的copy命令会保留被复制文件的修改时间,因而NMake不会认为这个main.cpp比main.exe更新,也就不会重新编译它了。这里改用type命令和重定向输出文件来模拟对文件的修改。当然,本书是为了方便演示才通过复制文件来修改源程序。正常开发中肯定会使用编辑器直接修改源程序,也就不会存在这种问题。

> type ..\按需编译\main.cpp > main.cpp
> nmake /F NMakefile
> main.exe
The 25th item of Fibonacci Sequence is:75025

第二次构建同样只会按需编译变更的文件,耗时很短。

make工具小结

make简约而不简单。它实在太灵活了,有时候会让人无所适从。尤其是面临跨平台需求时,其不足就很明显了。

与其说make是构建工具,倒不如说它是一个面向目标规则的命令行工具。归根结底,它只是根据规则推导出执行命令的顺序罢了,并非是一个专门针对构建某类程序的工具。换句话说,即使能够使得make在Windows操作系统上运行 ,从而避免NMake与之语法不同的问题,我们也不能够用简单的一份Makefile 来完成跨平台的C和C++程序构建。这里最明显的问题就是MSVC编译器的参数写法和GCC/Clang并不兼容,另外还有其他更多的与平台相关的问题。很多问题也许有一些解决方法,如使用Cygwin、MinGW等,但终究受限很多。总而言之,make并不是一个适合跨平台程序构建的工具。 jXopLDWtwipbnFVjLHTo34uZlCAc7X2AvQeYANSysVqhl2kB+q2uQG2XlIdGqKWJ

点击中间区域
呼出菜单
上一章
目录
下一章
×