第八章 生成数据
8.5 初始化数据的准则
在编程中,最大的错误原因之一便是对数据不恰当的初始化。开发有效地避免初始化错误 的技术,可以节约大量的调试时间。
不恰当初始化产生的问题,是由某一变量带有你没有预料的初值引起的。这可能是由下述 原因中的任何一种引起的:
・ 这个变量未被赋过任何值。其值是在程序开始运行时,由在它的内存中的位置偶然的 值决定的。
・ 变量的值已经过时了。变量在某点中被赋过值,但是这个值已不再有效了。
・ 变量的一部分被赋了值,而另一个部分没有被赋值。如果你使用的是指针变量,那么 常见的错误是用指针分配了内存,却忘记了对指针所指的变量进行初始化。这与对变 量根本不赋值的效果是一样的。
这种情况往往有几种表现形式。你可能初始化了结构的某一部分而没有初始化另一部分;
也可能会分配内存,然后初始化变量,但指向变量的指针却没有被初始化,这意味着你是随意 地拿一段内存并赋予它们一些值,这段内存可能是存储数据的,也可能是存储代码的,也可能 是被操作系统所占用的。这种错误的表现形式是多种多样的,而且它们之间往往每次都有着天 壤之别。这使得调试指针错误要比调试其它错误困难得多。
下面是如何避免初始化错误的一些准则:
检查输入参数的有效性。初始化的另一种有价值的形式是检查输入参数的有效性。在赋给 输入参数任何值之前,要首先确认它是合理的。
在使用变量的位置附近对其进行初始化。有些程序员喜欢在子程序开始的一个地方对所有 变量进行初始化,以下是用 Basic 语言写成的例子:
' initialize all variables Idx = 0
Total = 0 Done = False ...
'lots of code using Idx and Total ...
'code using Done while not Done ...
另外一些程序员则尽可能在每一次用到变量的位置附近对其进行初始化,下面也是用 Basic 写成的例子:
Idx = 0 'code using Idx ...
Total = 0 ── Total 在使用位置附近初始化 'code using Total
...
Done = False ── Done 在使用位置附近初始化 'code using Done
while not Done ...
第二个例子要好于第一个,其中有几个原因。在第一个例子中,当运行到使用 Done 的代 码时,Done 很有可能已经被改变过了。即使在初次写程序时不会出现这种情况的话,那么以后 你对其进行修改时,很可能会出现这种情况。第一种方法的另一个问题是,把所有变量集中进 行初始化,会给人以这些参数将在程序中随处都被用到的印象,而事实上 Done 只在程序结束 前才被用到;最后,当对程序进行改动时(这是很可能的,起码在调试时要修改),可能会把 Done 包含在循环中,从而需要对 Done 重新进行初始化,在这种情况下,第二个例子中的代码 不会有什么太大的变化。而第一个例子中的代码则可能会产生讨厌的初始化错误。
这也是邻近原则的一个例子:把相关的操作放在一起。这一原则也适用于把对代码的注释 放在相应的代码附近,把循环设定代码放在循环附近,把注释放在非循环代码中。
要特别注意计数器和累加器。变量 i、j、k 和 Sum、Total 等往往代表计数器和累加器。常 见的错误是在下次用到它们时,忘记了对其进行清零操作。
查找需要重新进行初始化的地方。首先问自己一下有哪些地方需要重新进行初始化。重新 初始化的原因可能是由于变量在循环中被用过多次,也可能是由于变量在对子程序的调用中间 要保持原值且需清零。如果需要重新初始化的话,要确保初始化语句是在被重复的代码段中。
对命名常量只初始化一次,用可执行代码初始化变量。如果要用变量来模仿命名常量,那 么在程序的开始对它们进行一次初始化是可以的,在 Fortran 中可以用 Data 语句来做到这点。
在其它语言中,可以在 Startup()子程序中初始化它们。
应在使用变量的位置附近的可执行代码对其进行初始化。最常见的改动之一是改动某一子 程序,变一次调用它为多次调用它。在 DATA 语句或 Startup()子程序中被初始化的变量,在程 序中不能被重新初始化。
按照所说明的对每个变量进行初始化。虽然其地位无法与在变量使用位置附近对其初始化 相比,但是按照所说明的初始化变量,仍然是防错性编程中的一件有力工具。如果你养成习惯,
那就可以有效地防止初始化错误。下例就保证了 student_name 在每次调用含有它的子程序时,
都将被重新初始化。
char student_name[NAME_LENGTH+1] = {'\0'}; /* full name of student */
利用编译程序的警告信息。许多编译程序都会对使用未初始化的变量进行警告。
设置编译程序使其自动初始化所有变量。如果你的编译程序支持这种选择项,那么让它对 所有变量进行初始化是非常简单的。但是,由于对编译程序的依赖性。当把程序移到另一台编 译程序不同的机器上时,则有可能产生问题。这时要确保你对所用的编译程序进行了注释,否 则是很难发现程序对编译程序有依赖性。
使用内存存取检查程序来查找无效的指针。在某些操作系统中,操作系统代码会自动查找 无效指针,在其它情况下,就只有依靠自己了。不过,你也不一定非得靠自己。可以买一个内 存存取检查程序来检查程序中的指针操作。
在程序开始初始化工作内存。把工作内存初始化到某一个值可以帮助发现初始化错误。可 以采取以下任何一种方法:
・ 可以用程序内存填充程序来赋予内存某一已知值,这个值用 0 比较好,因为它保证未 初始化指针指向低内存,比较容易发现它们,在 80X86 处理器中,16 进制 0CCH 比 较好,因为它是机器的断点中断码。如果是在调试中运行数据而不是代码,你就会进 入断点。使用值 0CCH 的另一个优点是在内存转储中它很容易辨认。
・ 如果你使用的是内存填充程序,可以每次改变一个填充值,用这种方法来检查一下运 行环境下隐藏的错误。
・ 可以用程序在软件运行时初始化工作内存。虽然使用内存填充程序的目的是把错误暴 露出来,但这种技术的目的则是隐藏它们。通过每次把工作内存由相同的值充满,可 以保证在每次运行时程序不会被启动时的随机因素干扰。
8.5.l 检查表 数据生成
生成类型
・ 程序中是否对每种可能变动的数据都使用了不同的类型?
・ 类型名称是面向客观世界中的实体类型而不是面向程序语言中的类型吗?
・ 类型名称是否有足够的表现力来帮助说明数据?
・ 避免重新定义已定义的类型了吗?
说明数据
・ 是否使用了模板为简化数据说明工作?并用其来统一说明形式?
・ 如果所用的语言支持隐式说明,是否采取了补救措施?
初始化
・ 是否每个子程序都对输入参数进行了检查以保证其有效性?
・ 是否在使用变量的位置附近对其进行初始化的?
・ 是否恰当地对计数器和指针进行了初始化?是否在必要时对其进行了重新初始化?
・ 在反复执行的代码段中,是否对变量进行了恰当地重新初始化?
・ 用编译程序编译代码时是否是无警告的?
8.6 小 结
・ 在你的工具箱中需要一张全部数据结构的清单,以便用最合适的方法处理每一种问题。
・ 建立自己的数据类型,以增加程序的可变动性,并使其成为自说明的。
・ 数据初始化很容易产生错误,因此应采用本章推荐的技术来避免由意外初始值所产生的错 误。