代码P280第二段
这里需要注意的是,数组的第一个元素的索引是0。也就是说,数组的第一个元素不是元素1。元素1实际上是数组的第二个元素。
程序员经常混淆这一点。当为第五个元素赋值时,这会导致语句编写如下。
代码P280第三行
这会导致应该赋给目标元素的值被错误地赋给非目标元素,从而造成严重的问题。事实上,给第五个元素赋值的正确说法如下。
代码 P281 第一行
指定数组下标时出错的主要原因是程序员的基本功不够扎实。为了避免这些问题,你必须训练自己在指定数组下标时能够区分代表顺序的序数词第一、第二、第三,以及代表数字的基数词一、二、三。你可以重新读一下你写的代码,检查一下数组下标是否正确。
指定数组下标的错误可能会破坏整个数据收集。即,将应赋给1号元素的值赋给0号元素,而1号元素仍保留其原始值。这与程序员的初衷相悖,而且很难定位这种数据混乱的根源。
这种由于从 1 开始计数数组而导致的错误也称为离一错误。顾名思义,就是少一的意思。从 1 开始计数可能会产生其他错误,例如无法处理数组的最后一个元素或引用不相关的数组,换句话说,引用的数组可能始终是目标数组的下一个数组。
尤其是在、for、do...循环语句中,更容易出现这种大小差异的错误。
...缩写...
计算数组所有元素的总和
...缩写...
根据注释,该语句要实现的功能是计算数组元素之和,所以应该从0号元素开始计算。但是数值是从1开始的,即从1号元素开始计算。这是一种典型的尺寸差异误差。该语句应重写如下,从 0 开始计数。
代码P282第二行
随着程序员编程经验的增加,此类错误的发生频率会逐渐减少。但新手程序员还是会一直犯错误,所以要时刻保持警惕。尤其是在写循环语句的时候,更需要注意。
2 替换字符串时必须使用括号
首先分析一下经常使用的宏函数。
代码P282第三行
从代码中可以看出,宏函数SQRT(x)可以计算x的平方值。从表面上看,这段代码没有任何问题。运行以下语句后,可以发现y值为100。
代码 P282 第 4 行
这是因为该语句被替换为以下形式。
代码 P283 第一行
如果将表达式作为宏函数中的参数传递,会发生什么情况?如果将以下表达式传递给宏函数,代码是否仍能正常运行?
代码P283第二行
上述语句替换为以下形式。
代码P283第三行
根据该表达式的运算符优先级,可以推断运算顺序如下,计算结果为230。
代码 P283 第 4 行
我们的预期结果应该是 900,但实际结果却是 230。
我们怎样才能避免这个问题呢?如下所示,定义宏函数时使用括号。
代码 P283 第 5 行
如上定义宏函数后,使用下面的语句来调用该函数。
代码 P283 第 6 行
正确答案是 900,因为语句被替换如下。
代码 P283 第 7 行
3. 文件必须是开放的且相关的
我编写了一个程序来处理一个需要一次保存大约 30,000 条数据的主文件。而且,主文件的特性要求多个程序同时拥有使用该文件的权限。
在项目接近尾声时,我开始进行全面的测试。这时,问题就出现了。所有的程序中,只有我写的程序有问题。我从头检查了程序代码,没有发现任何异常。虽然在各种编译器配置下不断编译执行,但界面总是显示“无法打开文件”的错误信息。
这实在令人费解。我花了整整一个星期的时间来研究代码。当然,整个项目推迟了一周。 1500行代码让我头晕、恶心,最后身心俱疲,几乎要放弃了。这时,我突然灵机一动:“也许不是程序的问题。”
如果不是程序的问题,那是什么问题呢?我开始查看可能与该主文件相关的所有程序。除了我自己写的程序之外,还检查了其他人写的程序。就这样,又一周过去了。
最终,我在一个新手程序员编写的一个非常小的程序中发现了这个问题。该程序打开相应的主文件后,并不会关闭该文件。我浪费了两周时间试图解决这个问题。结果整个项目延误了两周,最后我们还得支付延误赔偿金。
提示:应区分主文件和公共历史文件。以财务工作为例,每天收到的票据、单据是历史文件,是所有数据统计的基础资料。根据这些资料,可以计算出每天的累计统计结果,生成资产负债表和损益表,这个文件就是主文件。
当时软件工程还没有普及,很多程序员的基本功都没有扎实。但我认为,即使是现在,有些地方仍然如此。当时那个程序代码出了什么问题?虽然我记不起所有细节,但基本形状应该是这样的。
不关闭文件的示例
...缩写...
该代码尝试按如下方式处理它。
未关闭文件的程序伪代码示例
打开主文件。
无限循环
从文件中读取一行。
如果读取的行中没有数据,
通知“失败”并返回。
关闭文件。
这段代码有什么问题?问题是如果读取的行中没有数据,程序会立即返回到上层程序。即使没有读取到数据,在返回之前也应该关闭已打开的文件。否则,该文件将保持打开状态,其他程序将无法再次打开它。因此,菜鸟程序员应该从编写以下伪代码开始。
用于关闭文件的示例程序伪代码
打开主文件。
无限循环
从文件中读取一行。
如果读取的行中没有数据,
关闭文件。
通知“失败”并返回。
关闭文件。
只需在伪代码中添加一行“关闭文件”即可。菜鸟程序员只要多写一句这样的语句,就可以避免项目延误两周,而且不需要支付延误补偿金。菜鸟程序员应该根据上面的伪代码写出了下面的程序代码!
关闭文件示例
就因为缺少这行代码,就浪费了两周的时间。
...缩写...
();这短短的一行代码直接决定了项目的成败。因此,新手程序员应该牢记这一点:绝对不能出现文件打开但未关闭的情况!
4 不要忽略编译器警告错误
如果程序本身存在语法错误,编译器会在编译过程中发现并提示其存在。
所谓致命错误通常是指如果错误不被修复,程序就无法运行。但偶尔会出现一些致命错误,并不影响程序的运行,但通常会在运行过程中引起问题,所以最好在测试过程中发现它们。
这里的重点是警告错误。这种错误在程序运行过程中可能不会造成什么大问题,或者可能根本不会造成问题。解决这个问题意味着修复所有程序漏洞。
这种情况应该怎么办?当我在一个大型项目中进行单元测试时,我遇到了每次编译时都会出现的警告错误。当时同事查看编译器文档来查找这些错误的原因。编译器文档确实记录了相应的修复。
但编译器开发人员和文档编写人员并不是万能的神。他们只是会犯错误的普通人。无论我们如何按照记录的方法修复程序,都无法阻止警告错误的出现。
因此,我们尝试使用其他编译器来编译该程序。当使用其他编译器进行编译时,这些警告消息根本不会出现。因此,我们判断导致警告错误的罪魁祸首是编译器本身。
综合测试完成后,我们开始进行业务测试。在进行手工作业的同时,开始逐渐转变为利用电子系统的全自动工作模式。然后有一天,电子系统突然停止运转,这当然引起了混乱。订购公司甚至提出彻底取消电子系统的引入。
我们整个团队通宵达旦地分析原始代码,但找不到问题所在。突然,大家想到了一个想法:尝试使用特别不标准的数据作为输入值。然后我们开始尝试输入日常工作中几乎不可能出现的数据,从很小的值到很大的值,甚至输入一些根本不是值的字符作为数值数据。经过各种可能的尝试,我们终于找到了问题所在。
因为我们输入了一个我们没有想到的非常大的值,导致存储位置溢出,文件混乱,最后整个系统崩溃。这种现象称为缓冲区溢出或数据溢出。
我们最初使用的编译器是如何预测并警告出现此类问题的可能性的?原因不明。也许连编译器开发人员和文档作者都不知道。但可以肯定的是,如果我们当时没有忽略编译器的警告错误,我们本来可以提前阻止系统崩溃。
编码时掌握和预防运行时错误 程序运行时发生的错误称为运行时错误,它不同于编译错误和逻辑错误(程序流程中的漏洞引起的错误)。
编译错误主要是语法问题引起的,逻辑错误主要是程序逻辑或算法的设计缺陷引起的,运行时错误则与运行时环境密切相关。
例如,典型的运行时错误——堆栈溢出是由操作系统限制堆栈大小引起的。也就是说,只要计算机环境发生变化,堆栈大小也会发生变化,可能就不会再出现此类问题了。只需识别运行时错误的类型,并在编写代码时注意避免它们即可。编译器文档详细记录了运行时错误,因此最好提前阅读它。下面详细解释两个最具代表性的运行时错误。这两个错误很常见,只要能够避免它们,就可以编写出相当稳定的程序。
堆栈溢出计算机使用堆栈数据结构来管理临时存储空间。程序中使用的自动变量(大多数变量属于此)在声明时就保存到堆栈中,一旦离开自动变量的使用范围,就会被堆栈释放。查看下面的代码。
示例变量保存在堆栈中
将变量 var1 保存在堆栈上
将变量 var2 保存在堆栈上
堆栈释放var2
将变量 var3 保存在堆栈上
堆栈依次释放var3和var1
如上所示,变量随时在堆栈上保存或释放,因此不需要使堆栈无限。以现实生活中的仓库为例来考虑这个问题。如果仓库内的货物随时进出,而不是连续堆放货物,那么只需保证仓库的面积略大于货物进出过程中的最大堆积量即可。同样,堆栈是变量随时进出的地方,因此大小可以受到限制。
问题在于堆栈的大小有限。每个操作系统对堆栈的大小都有限制。虽然现在这个限制稍微放宽了,有些操作系统甚至允许用户独立控制和调整堆栈的大小,但在处理大容量数据时,仍然可能会出现堆栈溢出的情况。例如,开始在堆栈上保存自动变量后,一旦存储的变量的大小超过堆栈的大小,就会发生堆栈溢出。如果发生堆栈溢出,操作系统将强制终止程序。因为如果程序在栈溢出后继续运行,就会侵犯栈外的空间,影响其他程序。
这种堆栈溢出在使用大型数组或调用递归函数时很常见。当如下使用非常大的数组时,数组占据了大部分堆栈空间,留给其他自动变量存储的空间相对不足。对于这种情况我们将分别进行说明。
递归函数示例
...缩写...
如上所示,使用不断调用函数本身的递归调用后,变量和和变量会在堆栈中不断累积。随着调用次数的增加,累加变量的数量也随之增加,结果可能在某个时刻超出堆栈的容量。如果无限调用递归函数,肯定会发生堆栈溢出。递归调用次数越多,堆栈溢出的可能性就越大。
因此,在使用递归调用时,一定要仔细检查堆栈溢出的可能性。当递归调用次数没有精确限制并且递归仅在满足某个条件时才会终止时,此检查尤其必要。如下例所示。
示例退出条件决定递归调用的次数
...缩写...
退出条件
该代码的退出条件是与 sum 的值相等。如果 sum 的值很小,那么这段代码完全没问题。也许程序员已经假设总和值不会太大。但如果总和值非常大,会发生什么情况呢?如果总和值为 50 000 并且为 1 该怎么办?该函数将被调用 50,000 次。在此期间,该变量将在堆栈上累加 50,000 次。不,准确的说,是一直累加到栈溢出为止。
这样,如果递归调用的次数不只是一个预先确定的值,而是一个根据条件限制随时可能改变的值,那么堆栈溢出的可能性就非常大。这种情况下,最好在函数内部添加一条限制调用次数的语句。请记住,即使不会同时发生多个调用,情况也可能会随着程序运行状况的变化而变化。
除以 0
除0错误背后的原理非常简单。如下所示,将数字除以 0 会触发此错误。
代码P292第二段
没有程序员会故意将数字除以 0,但在非常复杂的代码中,除数可能会意外为 0。
假设您需要编写一个处理输入值的程序。程序的输入值至少为 1。程序员经常忘记在程序中明确指定此条件,一旦用户输入 0 并尝试除以该输入值,就会触发错误。
另一种情况在控制语句中很常见。想象一下,在 for 或 语句中,使用一个计算循环次数的变量(计数器)作为除数,与其他变量值进行除法运算。不存在计数器为0的情况吗?倒数时会发生什么?如下例所示。
该示例有除以 0 的可能性
...缩写...
除以0的可能性
...缩写...
这段代码中的值迟早会变成0,即迟早会被0除。这种情况大多隐藏在复杂的逻辑中,很难掌握。这就需要程序员清楚地意识到被0除的情况随时都可能发生,并仔细检查程序所有可能的运行条件,防止这种情况的发生。