第二课

也许现在我们写下的程序并没有实际的用处,当时随着我们学习的深入,你会发现你可以让自己的程序做很多的事情。那么在这一节里,我们主要来介绍两个方面的内容:注释和编译器在创建你的程序过程中的各个不同的阶段。在这里,我们还要探讨一下如何调试你的代码。

注释

注释其实就是你在代码中添加的所要注意的地方。注释可以有很多用处,例如:可以对某一段代码的意图进行解释,提供警告,临时禁用某一段代码。至于如何来使用注释,请参看下面的例子:

// 这是一个单行的注释.
// 同上. 在两个正斜杠之后所有内容都是注释的一部分.
int main(void)
{
    PushTheRedButton(); //在这一行中,两个斜杠之后的代码对程序运行没有任何影响.
    return 1;
}

注释也可是使用从 C 语言继承过来的多行注释,它们以 “/” 和 “/” 作为开始和结束的标志,和圆括号和大括号的用法相似,唯一的不同点是:不能够把两个多行注释叠加在一起使用。

/*-------------------------------------------------------------------
 RedButton.cpp
 如果在我们的代码中包含了编译器无法识别的函数,将会发生什么呢,那么下面这个代码
 就是一个例子.
---------------------------------------------------------------------*/
// 这是一个单行的注释.
// 同上. 在两个正斜杠之后的所有内容都是注释的一部分.
int main(void)
{
    PushTheRedButton(); // 在这一行中,两个斜杠之后的代码对程序运行没有任何影响.
    return 1;
}

那么接下来我们返回上一节的内容,看一下在编译器编译程序的过程中都经历了那些步骤。了解这些对于我们的变成是极为有利的,因为在编写代码的过程中将会出现各种各样的错误,而了解这个过程将可以让我们快速的找到问题的症结所在。

构建过程

在一个程序从源码开始编译的过程中,会有四个程序分别对它进行处理,它们分别是:预处理器,编译器,汇编器,和链接器。

第一步:预处理器

预处理器从开始接受源码到传递源码给编译器的过程中,只对源码做极少的处理。首先它将源码中的注释移去,然后将在 #include 后尖括号中所包含的头文件中的代码嵌入到源码中。还会有其他的一些指令的处理,这些指令的处理,我们将在以后在作讨论。

第二步:编译器

编译器将源码翻译为汇编语言。汇编语言还是可以被我们人类所理解的,但是它和机器语言已经是极为接近了。当然,使用汇编语言来编写程序是机器困难的,而且它会因为处理器的不同而不同,也就是与机器相关的。

第三步:汇编器

汇编器把自身生成的汇编代码转变为 object code(目标代码),并且把目标代码放置在以“a.o”为扩展名的目标文件内。目标代码已经是可以被机器理解和运行的指令了,然而它还是不能够运行的。尽管这些代码可以组成你的程序,但是目前它们还只是一些指令碎片,它们还需要被组织起来才可以运行。

第四步:链接器

链接器将目标文件和程序中所要使用的库文件中的指令碎片有机的组织起来,让它们成为一个可以运行的程序。

调试程序

由于人类的天性,每个人都会犯错,程序员也是这样。编写代码,然后调试代码总是一步一步来的,有时候也可以同时进行。那么,接下来就让我们开始调试代码的学习。

Bug 通常有两种类型:语法错误和语义错误。在语法上的错误时很容易被找到的,而且编译器通常会为我们完成这项工作。语法上的错误有大小写错误,括号的丢失或者多余以及函数名的输入错误等等。但是语义上的错误的发现是非常不容易的,因为这些错误存在于完全合法的代码的逻辑之中。在英文中 The oxygen censor on my car needed to be replaced 是完全符合语法规定和正确组织的,但是这句话是存在语义上的错误的,因为我们应该用 “sensor” 来代替 “censor”。语义上的错误有下面的几种情形:一些地方多了一个分号,数字的赋值错误,函数的返回值产生歧义等。

下面是一些常见错误的例子:

例 1

源码:

#include <stdio.h>
int main(void)
{
    return 1;
} }

错误:

foo.cpp:6: error: expected declaration before ‘}’ token

在这段代码中,产生了一个多出来的大括号。GCC 给出了一个正确的语法错误,而且还有两个提示:文件名和代码行号。由 gcc 给出行号和我们产生错误的行的行号并不总是一致的,但在这个例子中它们是相同的。

也许在这里,你会产生疑问,What in the world is a token, genius?,token 是一个语言元素。就像我们的语言中有单词和标点符号一样,计算机也有自己的词汇和标点。如果我们在一句话中接连使用两个逗号,这产生了一个标点使用错误,那么如果我们在 c++ 中多出来一个大括号,这也是 c++ 中的标点使用错误。

例 2

源码:

/*-----------------------------------------------------------------------
RedButton.cpp
/* 如果在我们的代码中包含了编译器无法识别的函数,将会发生什么呢,那么下面这个代码就是一个例子.*/
------------------------------------------------------------------------*/
// 这是一个单行的注释.
// 同上. 在两个正斜杠之后的所有内容都是注释的一部分.
int main(void)
{
    PushTheRedButton(); // 在这一行中,两个斜杠之后的代码对程序运行没有任何影响.
    return 1;
}

错误:

foo.cpp:7: error: expected unqualified-id before ‘--’ token

在上面这个例子中,错误提示给出的代码行号和实际错误的代码行号是不一致的。这个错误是由在顶部多行注释的结尾处出现的破折号所引起的。然而真正造成这个结果的是,在多行注释的内部添加了另一个多行注释。当预处理器将所有的注释移除之后,编译器接收到的代码是下面这个样子的:

----------------------------------*/
int main(void)
{
    PushTheRedButton();
    return 1;
}

那么接下来编译器不知道如何处理这些存在破折号的代码行,所以就报错了。

例3

源码:

int main(void)
{
    return 1;
}

错误:

/usr/lib/gcc/i486-linux-gnu/4.4.1/../../../../lib/crt1.o: In function `_start':
/build/buildd/eglibc-2.10.1/csu/../sysdeps/i386/elf/start.S:115: undefined
reference to `main'
/tmp/ccv39Cuo.o:(.eh_frame+0x12): undefined reference to `__gxx_personality_v0'
collect2: ld returned 1 exit status

这是一种不同类型的错误。是否还记得在每个程序中 main() 函数是必不可少的?但是这里我们没有使用—使用的是 main()。这个程序原本是有效地,所以它可以顺利的完成编译,但是当链接器准备将目标代码组织在一起的时候,它不能够找到一个必须存在的函数,所以就发飙了,进行了罢工。所以不管什么时候当你看到一个错误包含 undefined reference 时,这就意味着产生了一个链接错误。

解决由链接器产生的 undefined reference 错误并不是很困难。通常产生这一错误意味着有两个问题:你没有将所需要的库函数链接到主函数中,或者当你在创建程序的时候,一个源码文件被意外的删掉了。