正确编写C++代码的十大要诀

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;
}

6:使用using语句,特别是小程序

严格来说,常见的数据流(data-stream)对象cin和cout都是命名空间std的成员,要求按如下形式编写代码:

std::cout << "Hello." << std::endl;

真是多此一举啊!对绝大部分编程来说,我强烈推荐引入整个std命名空间,这样在cin、cout等前面就不必使用std::了。只消把下面这行放在每个程序开头:

using namespace std;

7:慎用全局变量,除用于函数间通信

这一条不用太多解释。程序员有时会自问该把变量声明成局部的还是全局的。规则很简单:如果变量是用来储存函数间通信用的信息,要么把这个变量作为参数(参数列表的组成部分),明确第传递这一信息,要么把变量弄成全局的。

要在几个函数间共享信息时,通常最实用的做法就是用全局变量表示。在语法层面上,这就意味着这些变量要在所有函数定义之外声明。

尽量少用全局变量的理由很明确:对于全局变量,一个函数的内部行为可能会且往往是无意地干扰其他函数的内部行为,这种大型程序里尤其明显。因此尽可能将绝大部分变量声明为局部的。毋庸赘言。

8:在for语句里使用局部变量

除去最古老的版本,在其他版本的C++中,局部化变量的最简方式是在for语句内部声明循环变量。该特性不是一开始就有的,因此我这样的老手还是会不时提醒你一下。

for (int i = 1; i <= n; i++) {  // 在循环内部声明i.
    cout << i << " ";           // 打印数字1到n.
}

对for循环而言,程序在循环之后很少会用到循环变量i的最终值(当然有时会用到)。一般来说,你会将诸如i的变量用作循环计数器,用完之后就丢掉。将i声明成循环内的局部变量不仅能有效节省一定的空间,也是更安全的编程方式。

这么使用for语句的一个好处是它会自动负责局部变量i的初始化。没错,它可能是多此一举,不过初始化局部变量是你应该做的,但如果函数的第一条语句是设定这个变量的值,该变量很可能未被初始化。不管怎样,最正确的程序应该像下面这样初始化局部变量:

void do_stuff() {
    int n = 0;   // 重复次数.
    int i = 0;   // 循环计数器.

注意,已初始化全局变量(包括对象)均为零值,而未初始化局部变量(包括对象)则包含无用信息,这里的无用信息是个技术术语,表示“无意义、无用值,有可能扰乱你的程序”。

9:切忌杯弓蛇影,滥用类和对象

这一条是概括性的理念指引。人们开始用C++编程时,常被告诫要将所有数据结构写成类,每个函数都弄成类的成员。

不过,上世纪90年代和本世纪初大肆宣传的面向对象程序设计(OOP),最近几年遭到一些抵触。事实上,对于只有控制台输出的简短程序,几乎找不到实例,证明类和对象能够缩减编程工作。相反,画蛇添足使用了类的程序反而占用更多空间。

那么,干嘛要用类呢?想想吧,面向对象的概念最早是由提出图形用户界面(GUI)的那班人搞出来的,既不是微软,也不是苹果公司,尽管他们声称是自己提出来的。这两种技术都起源于帕洛阿尔托研究中心(PARC)。

因此,一点都用不着惊讶,类和对象在图形或事件驱动的系统中用得最多,最得当。对象或类的实例是包含状态和行为的完整数据项,意味着它知道如何响应服务请求。这对GUI模型再合适不过了。

除此之外,我建议你掌握类和对象语法基础,这样就可以充分利用标准模板库(STL,Standard Template Library)。STL提供了大量丰富的功能,包括字符串、链表和栈等,可以简化许多编程任务。

10:类声明结尾用分号,函数结尾不用分号

一旦开始定义类,可别被语法搞晕了。

BASIC程序员有时会抱怨,“我可不想为什么时候用分号而费神。”这使得他们不喜欢Pascal。C和C++稍稍好些,至少分号(;)不是语句分隔符,只是语句终止符,用途始终如一。

但复合语句不用分号结束:

if (a == b) {
    cout << "a and b are equal.";
}

总的来说,规则就是用分号结束每个语句,但右大括号后面不能跟分号。因此,函数定义不是用分号结束的。

不过有一个例外:类声明(包括struct和union声明)必须用分号结束:

class Point {
  public:
    int x;
    int y;
};

在C/C++里,有关分号的完整语法规则总结如下:

  1. 用分号结束每个语句;
  2. 右大括号后面不能跟分号,但类声明除外。

小结

用C++编程(或者就此而言,可推广到所有语言)可谓是用到老,学到老。本文只给出了冰山一角;然而,在我可追溯至1980年代甚至更早的编程经历中,至少就个人而言,本文列举的问题都是反复遇到的。

其中更重要的想法是:根据需要选用正确的运算符(不要搞混=和==);关注数学运算里数据类型的作用;对while(n--)之类诱人的简写形式要格外小心;在for循环里使用局部变量;以及尽可能使用有意义的符号名。老实说,对于非常简单的程序,我有时也会图省事走捷径,不过当你投身复杂编程时,实践上述准则,也许做不到不让你心烦,但至少能帮你免掉很多麻烦。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值