1.5 神秘的数
神秘的数包括各种常数、数组的大小、字符位置、变换因子以及程序中出现的其他以文字形式写出的数值。给神秘的数起个名字。作为一个指导原则,除了0和1之外,程序里出现的任何数大概都可以算是神秘的数,它们应该有自己的名字。在程序源代码里,一个具有原本形式的数对其本身的重要性或作用没提供任何指示性信息,它们也导致程序难以理解和修改。下面的片段摘自一个在24×80的终端屏幕上打印字母频率的直方图程序,由于其中存在一些神秘的数,程序的意义变得很不清楚:

在这段代码里包含许多数值,如20、21、22、23、27等等。它们应该是互相有关系的⋯⋯但是⋯⋯它们确实有关系吗?实际上,在这个程序里应该只有三个数是重要的:24是屏幕的行数;80是列数;还有26,它是字母表中的字母个数。但这些数在代码中都没出现,这就使上面那些数显得更神秘了。
通过给上面的计算中出现的各个数命名,我们可以把代码弄得更清楚些。我们发现,例如数
字3是由(80-1)/26得到的,而let应该有26个项,而不是27个(这个超一(off-by-one) 错误可能是由
于写程序的人把屏幕坐标当作从1开始而引起的)。通过一些简化后,我们得到的结果代码是:现在,主循环到底做什么已经很清楚了:它是一个熟悉的从0到NLET的循环,是一个对数据(数组)元素操作的循环。程序里对draw的调用也同样容易理解,因为像MAXROW和MINCOL这样的词提醒我们实际参数的顺序。更重要的是,现在我们已经很容易把这个程序修改为能够对付其他的屏幕大小或不同的数据了。数被揭掉了神秘的面纱,代码的意义也随之一目了然了。


把数定义为常数,不要定义为宏。C程序员的传统方式是用#define行来对付神秘的数值。C 语言预处理程序是一个强有力的工具,但是它又有些鲁莽。使用宏进行编程是一种很危险的方式,因为宏会在背地里改变程序的词法结构。我们应该让语言去做正确的工作。在C和C++ 里,整数常数可以用枚举语句声明,就像上面的例子里所做的那样。在C++里任何类型都可使用const声明的常数:
![]()
在Java中可以用final声明:
![]()
C语言里也有const值,但它们不能用作数组的界。这样,enum就是C中惟一可用的选择了。使用字符形式的常量,不要用整数。人们常用在<ctype.h>里的函数,或者用与它们等价的内容检测字符的性质。有一个测试写成这样:
![]()
这种写法将完全依赖于特殊的字符表示方式。写成下面这样更好一些:
![]()
但是,如果在某个编码字符集里的字母编码不是连续的,或者其中还夹有其他字母,那么这种描述的效果就是错的。最好是直接使用库函数,在C和C++ 里写:
![]()
或在Java里面:
![]()
与此类似的还有另一个问题,那就是程序里许多上下文中经常出现的0。虽然编译系统会把它转换为适当类型,但是,如果我们把每个0的类型写得更明确更清楚,对读程序的人理解其作用是很有帮助的。例如,用(void*)0或NULL表示C里的空指针值,用‘\0’而不是0表示字符串结尾的空字节。也就是说,不要写:
![]()
应该写成:
![]()
我们赞成使用不同形式的显式常数,而把0仅留做整数常量。采用这些形式实际上指明了有关值的用途,能起一点文档作用。可惜的是,在C++ 里人们都已接受了用0(而不是NULL)表示空指针。Java为解决这个问题采用了一种更好的方法,它定义了一个关键字null,用来表示一个对象引用实际上并没有引用任何东西。
利用语言去计算对象的大小。不要对任何数据类型使用显式写出来的大小。例如,我们应该用sizeof(int) 而不是2或者4。基于同样原因,写sizeof(array[0]) 可能比sizeof(int) 更好,因为即使是数组的类型改变了,也没有什么东西需要改变。
利用运算符sizeof常常可以很方便地避免为数组大小引进新名字。例如,写:
![]()
这里的缓冲区大小仍然是个神秘数。但是它只在这个声明中出现了一次。为局部数组的大小引进一个新名字价值并不大,而写出的代码能在数据大小或类型改变的情况下不需要任何改动,这一点肯定是有价值的。
Java语言中的数组有一个length域,它给出数组的元素个数:

在C和C++里没有与.length对应的内容。但是,对于那些可以看清楚的数组(不是指针),下面的宏定义能计算出数组的元素个数:
![]()

在这里,数组大小只在一个地方设置。如果数组的大小改变,其余代码都不必改动。对函数参数的多次求值在这里也不会出问题,因为它不会出现任何副作用,事实上,这个计算在程序编译时就已经做完了。这是宏的一个恰当使用,因为它做了某种函数无法完成的工作,从数组声明计算出它的大小。
练习1-10 如何重写下面定义,使出错的可能性降到最小?








