5.2.构建C++程序
C++是典型的“编译型”语言。
对于编译型语言,我们可以笼统地将“代码变成程序”这个过程,称呼为“编译”。不过,如果细分起来,这个过程还包括了以下三个子过程:
此时,为了避免混淆,我们需要将“代码变成程序”这个完整的过程,称为“构建/build”。上面的子过程还可以被细化,不过对于普通编程工作,了解这三方步就够了。
5.2.1. 预编译
“ 预编译”有时候也被称为“预处理/preprocessing”,就好像烹调大师在做菜之前,需要先将菜洗好,切好;需要将待用的佐料准备齐全,并且放在 随手可得的位置等等,编译器在编译程序之前,需要先读入待编译的源代码,在内存中,整理格式,转换部分内容,以利于其后内容。
比如,C++用于进行逻辑“或者”运算的符号为“||”,但考虑某些国家的电脑键盘缺少“|”字符,C++也允许使用 单词“or”表示该运算符;那么在预编译时,就可以将这些代替符号在内存中映射成统一代号。
更加常见的内容还有,C++允许通过一些“预编译指令”来实现部分代码在某个编译环境下临时无效(不需要参加编译),这些无效代码,在预编译时,可以在内存中清除内容,比如
001 //通过宏定义,检查当前是否在Windows操作系统下编译
002 #ifdef __WIN32__
003 std:: string os = “Windows”;
004 #end
如果当前正好是在Linux操作系统下编译以上代码,则003行的代码内容被剔除。更易于理解的还有,注释也会被清除,比如001行代码。
〖小提示〗:宏能够检测当前操作系统?
宏只是几个字符组成,它没有这么神奇的力量可以检查当前操作系统。事实上是人(程序员)根据当前编译环境,从而预定义某些宏。
不过预编译过程最主要的,也是我们最需要小心的地方,是C/C++中的“宏替换”。所谓的“宏”是指C/C++允许以一个(通常是简短的,有意义的)标记符,来表示另一一段(通常是冗长的,或者无意义的)代码,比如:
#define PAI 3.14159265
这段代码用“PAI”这个标志符号,代表长长的圆周率“3.14159265”。那么在预编译时,除非遇上取消该宏定义的代码,否则之后遇上的“PAI”标志符,被替换成具体的3.14159265这个数值。比如
int radius; //半径
cout << “请输入半径:”;
cin >> radius;
double circumference = 2 * PAI * radius;
cout << “周长= ” << circumference << endl;
通过预编译之后,转换成
int radius;
cout << “请输入半径:”;
cin >> radius;
double circumference = 2 * 3.14159265 * radius;
cout << “周长= ” << circumference << endl;
5.2.2. 编译
C++的代码被转换成机器语言,在编译这个阶段中完成。我们程序中有任何语法上的错误,会被编译器查找出来。还有某些在语法上无误,但编译器认为“形迹可疑”的代码,也会以“警告”的形式提出。
对于初学者,往往写完代码用了10分钟,对付编译器给的问题,却用上120分钟。
这样一个重要的阶段,或许我们应该用大量的篇幅进行描述?那已经是另一门课程,有兴趣的读者请购买相关书籍阅读。
〖课堂作业〗:观察编译错误消息
请使用Code::Blocks打开“Hello world”经典版项目,然后注释掉第4行代码,效果如下:
//using namespace std;
重新编译该项目,观察编译器给出消息,并加以理解,注意区分其中的“error”和“warning”类型。
下面是该课堂作业的答案:
编译信息及相关解释(为节省篇幅,省略文件路径信息):
||=== HelloWorld, Debug ===|
.../main.cpp||In function `int main()':|
(main.cpp文件的中main函数中:)
.../main.cpp|8|error: `cout' was not declared in this scope|
(main.cpp第8行,错误:’cout’ 在当前范围内,找不到它的声明)
.../main.cpp|8|error: `endl' was not declared in this scope| .../main.cpp|8|warning: unused variable 'cout'|
(main.cpp第8行,警告:cout 这个变量没有被用上)
.../main.cpp|8|warning: unused variable 'endl'|
(main.cpp第8行,警告:endl 这个变量没有被用上)
||=== 已完成构建: 2 个错误, 2 个警告 ===|
编译器先是报一错,说是“’cout’ 在当前范围内,找不到它的声明”。cout在声明在名字空间std之中,但是我们刚刚注释掉对该名字空间的使用,所以编译器就会找不到cout的声明。
紧接着,编译器既然找不到cout和的声明,它就会试着装装糊涂,将cout当成一个变量对待,这样一来,它又觉得不太对头,这个cout变量没有被(正确地)使用过,于是它又报了一个警告。
编译有问题了,仔细阅读编译消息,再对照实际代码……脑袋一拍:“哎呀!原来是……”迅速修改代码,再尝试编译……通过了!真开心;还是有错,真着急——这就是程序员每天都在经历的生活。
〖重要〗: 有关编译信息的重要注意事项
1、警告消息同样很重要,我们之前已经了解过这一点。
2、编译器并不“神”,所以它所提示的行号,有时很准确,但有时完全不靠边,如何准确定位出错位置,既要靠你的对C++语言的熟悉程度,还要靠你的经验。
3、编译通过了,只表示语法都正确了,而不代表这就是一个正确的程序。
5.2.3. 链接
谈到链接,首先会有困惑:“都已经编译成机器语言了,不就完事了吗?链接干什么?”
原来,C++的编译过程,是单个单个源文件进行的。假设为了写一个程序,你的有一个名为k的项目,它包含了两个源文件:a.cpp和b.cpp,那么编译过程将是:
第一、 预编译a.cpp,编译a.cpp,生成目标文件:a.o;
第二、 预编译b.cpp,编译b.cpp,生成目标文件:c.o;
第三、 链接a.o和c.o,出现可执行文件:k.exe。
其中的“目标”文件,有时也被直观地称为“中间文件”,原因在于当它在被链接成可执行文件之后,就可以被删除。
〖小提示〗:目标文件及可执行文件的扩展名
目标文件和可执行文件的扩展名,依赖于操作系统或编译器,.obj和.exe是Windows操作系统下的默认风格,但对于mingw32下的g++编译器,生成的目标文件扩展名为“.o”。
如此,“链接”所做主要工作之一,就变得容易理解了:编译时,每一个源文件虽然都被编译成机器语言了,但却是零散的多个目标文件,于是链接器出场 ,将这些目标文件“拼接”、或者“组装”成一个完整的可执行文件。
〖轻松一刻〗:“拼接”工作最害怕什么?
小时候,我是一个“拆卸狂”,最喜欢拆完又装的就是父亲床头的闹钟,但第一次拆完又装后,它多了几个零件,第二次偏偏少了几个零件,终于闹钟被我折腾到死无全尸。
如今我也当上爸爸,小女在两岁时就喜欢玩我的笔记本电脑,三岁时,我发现她在抠本本的键盘,我惊出一身汗,立刻就上玩具市场买了数套拼图游戏回家。
说起来,编译器是非常“八婆”的家伙,它经常毫不顾及我们的脸面,挑出代码中的一大堆错误。链接器就比它酷多了,它最喜欢的挑的错,只有两个:其一是:“少了个XXX”;其二:“多了个XXX”。这里的“XXX”并不是指源文件,而是指代码中的对象,最常见的就是函数。
继续那个k项目包含a.cpp和b.cpp的例子。假设我们在a.cpp中,要用到一个函数叫“foo()”。编译器并不要求我们立即找到这个函数的实现,相反,我们只需要给出这个函数的“声明”即可。所谓的“声明”,就是告诉编译器,这个“函数长什么样子”。
那么,谁负责最终找到“foo()”函数的真身呢?“链接器”是也。链接器发现a.o里要用到“foo()”这个函数,于是它先在a.o里查一查,没找到;于是又到b.o里再找找,还是没找到,这下它急了,于是就报怨了一句“少了个foo()!”。
理解了这个“少了个XXX”,“多了个XXX”的错误也容易理解了。想像链接器在a.o里找到“foo()”,正得意呢,可等在拼装完了时,发现在b.o里居然还有个一模一样的“foo()”,它就会叫出来:“哇,竟然多了一个foo()!”
让我们修改“Hello world”经典版代码如下:
/*这是我的第一个C++程序*/
#include <iostream>
004 using namespace std;
006 void foo();
int main ()
{
010 foo();
cout << "Hello world!" << endl;
return 0 ;
}
004行,记得你去除了前一小节的注释。
006行,我们声明了一个“foo()”函数。
010行,我们调用了这个并不实际存在的“foo()”函数。
重新编译这个项目,会发生链接错误。请在消息栏(按F2确保显示)切换到“构建记录”页面,我们首先在记录中查看到以下信息:
-------------- 构建: Debug 在 HelloWorld中 ---------------
正在编译: main.cpp
正在链接 控制台可执行: bin/Debug/HelloWorld.exe
信息显示,“编译”过程已经完成,问题出在“链接”过程。将消息栏切换到“构建信息”,看到以下出错消息:
obj/Debug/main.o||In function `main':|
.../main.cpp|10|undefined reference to `foo()'|
||=== 已完成构建: 1 个错误, 0 个警告 ===|
这回,问题是从“obj/Debug/main.o”内发生,当然,链接器也非常努力的提醒我们,main.cpp的第10行可能有点问题。
[回到目录]