《深入浅出C++(第2版)》作者原文:Top Ten Tips for Correct C++ Coding

感谢李松峰,部分用词参考了正确编写C++代码的10个要点(2-1)一文。

enter image description here By Brian Overland @May 17, 2011

Brian Overland,曾在微软工作十年,《深入浅出C++(第2版)》C++ Without Fear: A Beginner's Guide That Makes You Feel Smart, 2nd Edition)一书作者,分享了自己数十年来编写和调试C++代码悟到的,10条来之不易、省时省力的要诀。

我第一次接触C语言是在几十年前(好吧,我知道这让我很显老)。后来我又学了C++。真希望那个时候有人能带着我避开那些显而易见的陷阱,这样也许我可以免受不少煎熬。

至少现在我可以提供几条这类必需的(抱歉用这个词)指引。本文不是C++教程,而是面向C++学习者的指南。不过老实说,对于C++,总有学不完的东西。

要想编写优美专业的C++代码,且容易维护,也不大需要调试,务请牢记以下十条要诀,为此本文的要求有些比我在《深入浅出C++(第2版)》(C++ Without Fear (2/e))里写的还要严格。

这些准则不分先后(抱歉,David Letterman),不过前几条准则更多是针对困扰初学者的错误。


1:不要搞混赋值(=)和相等测试(==)

这一条非常基础,尽管可能难倒过福尔摩斯。下面这段代码看似正常,而且C++要是更像BASIC的话,编译和运行也没问题。

if (a = b)
    cout << "a is equal to b.";

正因为上面的代码表面上看起来没什么问题,在大型程序里,它引起的逻辑错误需要耗费数小时才能定位,除非你事先对它有所警觉。(因此我在调试程序时第一时间会排查这类错误。)在C和C++里,下面的表达式并非测试两个变量是否相等:

a = b

这个表达式实则是将b的值赋给a,然后再求出所赋的值。

问题在于a = b的求值结果并不总是合理的真/假条件,除了我后面将提到的一大例外。但在C和C++里,任意数值都可以用作if或while语句的条件。

假定a和b的值都是0。前面给出的if语句效果等同于将b的值传给a;然后表达式a = b求值为0。而值0等于false。结果,a和b相等,却打印了错误的消息:

if (a = b)     // 这样a和b显然相等...
    cout << "a and b are equal.";
else
    cout << "a and b are not equal.";  // 结果打印的却是这条消息!

解决办法自然是根据需要使用相等测试。注意要用两个等号(==),在条件里用这个运算符才对。

// 正确版本:
if (a == b)
    cout << "a and b are equal.";

2:杜绝“幻数”

所谓幻数(又称魔数、魔术数字)是指散落于程序中未加注释的字面值(literal number)。多数老到的程序员更乐见程序里只有诸如MAX_ROWS、SCREEN_WIDTH的符号名。

总之,专业的计算机程序员——包括一些对数学情有独钟的人——其实讨厌数字!

个中缘由与历史有点关系。遥想上世纪40年代,那时只能用各种比特位组合形成的机器码编程。程序员生不如死,必须不停地转换这些组合。随后汇编语言出现,编程用上了符号名,比之前要容易千百倍。

即使到了今天,程序员们也不喜欢这样的声明:

char input_string[81];

其中81是个“幻数”。它从哪儿来?更好的做法是用#define或enum语句限制数字的使用。

#define SCREEN_WIDTH 80

比起81,SCREEN_WIDTH更加一目了然,如果将来打算重新设定这个宽度,你只需修改一行代码。随后,宽度修改会自动体现到如下语句:

int input_string[SCREEN_WIDTH + 1];

3:不要依赖整数除法(除非有意为之)

不需要储存小数部分时,优先选用整型(在C/C++里,即int,以及short、long还有long long),有许多充足的理由,这里就不具体展开了。

不过,有时整数是一个含有小数的更长表达式的一部分。下面的代码来自拙作《深入浅出C++(第2版)》(C++ Without Fear):

cout << results / (n / 10);

这个程序会生成0到9之间的随机数,每个数字出现的概率应该是1/10。这里是用每个数字实际出现的次数除以期望出现次数N/10。比如,如果3的results(实际出现的次数)是997,而总共进行了10 000次测试,那么就是用997除以期望出现的次数1000。

但results、n和10都是整数。结果,997除以1000得到零!

等等,我们本来预想的是0.997,到底怎么回事?

整数除法向下舍入,得到一个最接近的整数。余数则扔掉了。这也不一定是坏事。C++提供了两个独立的除法运算:除和求余。

int dividend = n / m;   // 求比例.
int remainder = n % m;  // 求余数.

对了,前面那行代码使用了一个“幻数”——10,我晕!不过,下面我们就来说说这个更麻烦的问题:数据丢失。

4:利用数据类型提升控制结果

在混有整数和浮点数的表达式里,整型操作数会被提升为double型。因此,下面的表达式会产生我们想要的结果:

cout << results / (n / 10.0);

注意10.0的小数部分虽然为0,但仍以double型储存,在C++中,这会导致n和results也提升为double型,然后执行浮点除法。

利用数据强制转型也可取得以上效果:

cout << results / (n / (double) 10);
cout << results / (n / static_cast<double>(10));

5:不要使用非布尔条件(除非格外小心)

C语言的设计初衷是为了编写操作系统,因此它赋予程序员很大的自由,不仅可以在机器码层面(借助指针)操控数据,还可以写出简写形式。简写形式对初学者很危险,但对知道该怎么用的程序员来说,有时非常便捷。但愿他们心甘情愿地活在危险之中。

下面这段代码展示了其中最优雅的一项技巧,也是实现“某个动作执行N遍”的捷径:

while (n--) {
    do_something();
}

还可以进一步简写如下:

while (n--) do_something();

同样,不管怎么写,我们都是在利用C和C++里任意数字值都可用作条件这一规则。循环每执行一次n都会递减1,直到n为0,循环结束。但问题在于:要是n初值为负会怎么样?我刚才展示的代码就会陷入死循环,或者至少一直循环至最小的负值,直到溢出为止。那会让你郁闷透顶。

总之,只有那些严格意义上的布尔(即真/假)表达式才能用作条件。

while (n-- > 0) {
    do_something();
}

不过也有一大例外。在面对操作失败指针被置为NULL(也即0)时,这种简写形式很有用处。NULL实际上等同于false。空指针还可用于链接表,指示链表结束,节点的next_node成员指向空(nowhere,上世纪五六十年代,说的是指向Nowheresville)。在下列代码中,空指针意味着文件打开失败:

if (! file_pointer) {
    cout << "File open failed.";
    return ERROR_CODE;
}

顺便提一句,有时你可能会在条件里用到赋值操作:

if (! (file_pointer = open_the_file(args))) {
    cout << "File open failed.";
    return ERROR_CODE;
}

相关文章:

本文参加 Translate Geeks to Chinese 翻译活动