您的位置:寻梦网首页编程乐园VB 编程Visual Basic 电子教程>VB 程式设计内功讲座

VB 程式设计内功讲座(叁) - 王国荣

除错—增加内功必经之道


几乎所有角色扮演的电动都有一个共通的特点,那就是主角一开始都很弱,但随着打败的敌人越来越多,主角的功力也会越来越强,程式设计的道理也是相同的,再多的理论都比不上动手去写程式,而写程式所面临的主要敌人就是bug,同样的随着除过的bug越来越多,程式设计者的功力也会越来越强。

Bug 是什麽?有一句名言最能诠释bug的意义:「电脑是按照指令行事,而不是人的意志」,当人将其意志转化成指令下给电脑之後,而电脑执行的结果与人的意志不符时,便是bug。

怎样下指令给电脑呢?若是一般的使用者,使用的是作业系统所提供的指令(例如copy指令)或应用程式所提供的操作介面(当使用者操作应用程式时,应用程式即已代替使用者下指令给电脑了)。

然而对程式设计人员而言,下指令给电脑的过程就比较复杂了,首先必须选取适当的程式语言,然後将自己期望电脑做的事情表示成程式语言,接着再利用程式开发工具编译成执行档,最後才经由作业系统所提供的指令载入执行档,由电脑开始执行,理论上,以上的每一个过程都可能发生错误,例如程式语言选错了、程式写错了、编译器有bug而编译出错误的执行档、作业系统有bug… ,想一想还真可怕,到处都可能有bug,这也莫怪有人要说「软体是高风险的事业」。

不过本文所讨论的范围仅局限於「程式写错」的部分,首先我们只能假设编译器是没有错的,作业系统是没有错的,这就好像我们搭飞机以前必须假设飞机是不会出事的一样,如果您十分在乎飞机、编译器、作业系统的极少数出错情况,那麽笔者只能说,别搭飞机,也别当个程式设计人员。

有句话说:「人非圣贤,孰能无过」,而笔者想说的是「程式设计无圣贤」,从开始写第一行程式就不会出错,笔者未曾听过啊!知错能改,乃程式设计之根本大法也。记得以前在学校的时候,老师总喜欢讲「知错能改,善莫大焉」,当老师者焉知数学题作错了,就是不懂啊!不懂者又如何知错,又如何能改呢?对程式设计来说,道理也是相同的,「知错能改」说得简单,做起来却十分困难,笔者写了十几年的程式,还是必须面临bug上身的问题,不过也正因为如此,本期要与您谈一谈除错的方法!

肉眼除错法

肉眼除错指的是将程式印出来或是盯着萤幕一行一行检查,这听起来有点不太入流,主要的缺点是这个方法的除错效率不高,尤其对於自己写的程式,总认为程式「应该」按照自己的意思执行,而没有看出程式「实 际 上」是怎麽执行的。

肉眼所观察出来的错误,通常都还不是十分肯定,因此接下来是修改程式、编译、联结、测试,然後才能确定是否找到了错误,如果肉眼除错一直无法找出错误,那麽「侦错、修改程式、编译、联结、测试」等动作就得一再重来,非常浪费时间。

肉眼除错与程式设计人员对程式语言的了解也有极大的关系,笔者并不全然反对这个方法,因为在侦错的过程中,程式员通常会去思索程式为什麽错了,这对程式语言的了解颇有帮助,而且也可以训练程式员对於错误的敏锐性,不过就像前面所说的,这个方法的效率不高,除了给自己这方面的训练之外,别忘了时程控制」在程式开发中也是非常重要的一件事,因此请再参考以下更有效率的除错技巧。

使用VB的侦错功能

VB所提供的侦错功能相当丰富,值得一试。

侦测的对象与侦测的工具

程式错了,侦测的对象不外乎程式码或资料(变数及物件),而这两者的关系是:程式码是「因」、资料是「果」,虽然说我们要找出错误的因,但是在侦测的过程中,却必须从果来着手,因为果比因来得容易观察,直到发现某一资料的结果错误时,再逐步清查错误原因。

在检视「果」(资料)的功能中,VB所提供的有:

◇ 即时运算视窗:即时运算视窗是一个BASIC语言的编译器,我们在里面所输入的指令将会立刻被执行,因此当我们怀疑某个变数可能有错时,只要利用「?变数名称」即可加以检验,此外,它也可以用来执行副程式、函数、及合法的VB叙述。

◇ 区域变数视窗:当我们将程式中断於某一个程序(包含副程式、函数、及事件程序)时,区域变数视窗就会显示该程序的所有区域变数,如此一来,不必在即时运算视窗输入「?变数名称」即可以看到区域变数的内容。区域变数视窗除了会显示某一程序的区域变数之外,也会显示该程序所在模组的全域变数。

◇ 监看视窗:区域变数视窗会显示所有区域变数的内容,但如果变数过多,便不容易观察,监看视窗则只显示特别观察的变数。

◇ 程式视窗:程式视窗是检测程式码执行过程的视窗,不过VB5特别增加了一个方便的功能:在中断模式底下,只要将滑鼠游标移到变数的上面,程式视窗即会以黄色条块显示该变数的内容。

以上的功能座落在功能表的「检视」栏底下。

在检测「因」(程式码)方面,VB所提供的功能有:

◇ 设定中断点:可将某一行叙述设定成中断点,则当程式执行到此一叙述时,程式即会进入中断模式,而只有在中断模式底下,我们才能够利用即时运算视窗、区域变数视窗、监看视窗、及程式视窗检视变数的内容。

◇ 逐行执行:每执行一行叙述程式即进入中断模式,此一功能可让我们检视每一行叙述执行前後的结果。

◇ 逐程序执行:与逐行执行类似,但遇到程序的呼叫时,会执行过被呼叫程序的所有叙述,才中断於呼叫程序端的下一行叙述。

◇ 跳出程序:当程式中断於某一个程序,而我们不想再逐行执行时,此时可以使用此一功能一路执行完此一程序,而让中断点回到呼叫端的下一个叙述。(请注意此一功能并非脱离程序不执行)

◇ 执行至游标处:在某一程序中,既不想逐行执行,也不想跳出程序,可以先将输入游标设定於某一叙述(在叙述上面按下滑鼠),然後选取此一功能,则程式会执行到输入游标处,方才中断。

◇ 设定下个执行点:此一功能有点像是VB的Goto叙述,将会跳过(不执行)中间的所有叙述。

◇ 呼叫堆叠:可显示程序呼叫的所有历程。

以上的功能除了「呼叫堆叠」位於功能表的「检视」栏底下之外,其他功能则座落在功能表的「侦错」栏底下。

中断程式的几种方法

程式执行时是无法侦错的,我们一定要先让程式停下来,才能检测各个变数的内容,所以使用VB侦错功能的第一步是让VB程式能够停下来,而以下是中断程式方法:

设定中断点

开启程式视窗,在我们希望中断的叙述上面按下F9,则将来程式执行到此一叙述时,即会进入中断模式,此时我们可以使用各种检视变数的功能来检查截至此一叙述以前执行的结果。(特别注意:所谓中断点是还没有执行的叙述,不是已经执行而停下来的叙述)

设定中断点的另一个方法是使用Stop叙述,当程式执行到Stop叙述时,也会进入中断模式。使用Stop叙述有一得及一失,得的方面是中断点可以储存在程式之中,不像F9所设定的中断点,在专案结束时即告消失,失的方面则是含有Stop叙述的程式被编译成执行档之後,只要程式值执行到Stop叙述即会结束执行,这使得我们在程式编译成执行档之前必须把所有的Stop叙述拿掉。

发生了可处理错误

程式的错误按严重性可分成「不可处理」与「可处理」两种,在Windows环境底下,有时候我们会看到如图-1的讯息窗,当程式出现此一讯息窗时,就难逃被踢出系统的命运,此为程式发生了不可处理的错误。

图-1 不可处理的错误所显示的讯息窗

不可处理的错误最常发生於程式企图将资料写入於不该写入的记忆体之中,一般而言,C/C++ 程式由於可直接操作记忆体的位址,最容易发生这种事情,VB程式则由於无法直接操作记忆体的位址,绝少发生类似於C/C++的错误,因此本文只讨论可处理的错误。

发生可处理的错误时,VB会显示错误的讯息窗,例如某一阵列X的注标范围是0到3,则执行「X(4) = 123」时将会产生如 图-2的可处理错误。

图-2 VB 所显示的可处理错误

此时我们若按下「侦错」钮,即可使程式进入中断模式,而此时中断点将停留在发生错误的叙述上面。由於可处理的错误是VB侦测出来的,接下来进入中断模式之後,我们甚至可以把错误的叙述改掉,然後继续执行程式。

按下 Ctrl+Break 键

不管任何时间,只要我们按下 Ctrl+Break 键,则VB程式即会进入中断模式,此一方法在程式进入无穷回圈时最为实用,例如以下程式:

Dim A As Single

A = 0

Do

A = A + 0.01

Loop Until A = 1

乍看之下,程式并不会进入无穷回圈,但实际上却因为电脑处理小数点时会有些微的误差,以致A不会刚好等於1,而使得程式进入了无穷回圈,当程式进入无穷回圈时,VB可以说是动弹不得,唯独Ctrl+Break 可以中断程式。

以 F8 键启动程式的执行

在 VB 的工作环境底下,按下 F5 执行程式是最普遍的方式,而按下F5 执行程式必须采用上述的方法才能够中断程式,如果我们一开始就按下F8,也可以执行程式,但将来只要执行一行叙述,程式便会进入中断模式,接着在中断模式底下,只要我们继续按下F8,则仍然维持一次执行一行叙述的方式,这是VB 所提供的逐行执行的功能。

侦错案例研究

VB 所提供的侦错功能的确不少,当我们进行程式的侦错时,该如何选择适当的功能呢?以下让笔者以实例的案例来说明。

「逐行执行」与「区域变数视窗」的搭配

对於刚刚写好而又没有呼叫其他自定程序的程式而言,使用逐行执行来测试最为适合,举例来说,以下是一个互换两个变数内容的副程式:

Sub Swap(A, B)

Dim temp
temp = A
A = B
B = temp

End Sub

假设我们不确定副程式执行的结果是否正确,可以采用逐行执行的方式,如下:

1. 输入以上副程式,然後在表单上布置一个命令钮,并且在命令钮的 Click 事件程序中撰写以下程式:

Private Sub Command1_Click()

X = 100
Y = 200
Swap X, Y

End Sub

2. 按下 F8 执行程式,然後按下表单上的命令钮,此时程式视窗会弹到萤幕的最前端,而在程式视窗中标示着箭号的叙述为下一个被执行的叙述,如图-3,持续按下 F8,直到程式进入 Swap 副程式为止。

图-3 程式视窗中下一个被执行的叙述

3. 进入 Swap 副程式之後,选取 VB 功能表的「检视/区域变数视窗」,此时所调出的区域变数视窗如图-4。

图-4 利用区域变数视窗检验变数的内容

4. 接着还是以 F8 逐行执行程式,而由於按下 F8 只执行一行程式,因此我们可以在每执行一行程式之後,即检验区域变数视窗中 A、B、temp 变数的结果是否正确,若有错误便可以轻易地找到。

设定中断点的技巧

如果程式比较大或含有回圈,就不适合采用逐行执行的侦测技巧,此时我们通常会每隔一段程式设定一个中断点,并且在程式执行到中断点时检验变数的内容是否正确,如果正确便继续向下测试,如果不正确,即表示这个中断点与上一个中断点之间的程式有错。

接着还是让笔者以实例来说明侦测的过程,假设有一副程式如下,此一副程式的作用是以空白为分割字元,将参数 Sin 字串分割成多个字串,并且存放在 Sout 字串阵列中,而参数 N 则会记录放到 Sout 之中的字串数目,但这个副程式有 bug,以下让我们找出 bug 所在,并且加以修正:

' 参数一 Sin : 输入的字串
' 参数二 Sout: 传回的字串阵列
' 参数叁 N : 传回的字串数目
Sub Parse(ByVal Sin As String, Sout() As String, N As Integer)

Dim pos As Integer
N = 0
S = Trim(S)
Do

pos = InStr(Sin, " ") ' 找出空白字元的所在位置
If pos > 0 Then ' 如果含有空白字元

N = N + 1
Sout(N) = Left(Sin, pos - 1)
Sin = Trim(Mid(Sin, pos + 1))

End If

Loop Until pos = 0

End Sub

1. 输入以上副程式,然後在表单上布置一个命令钮,并且在命令钮的 Click 事件程序中撰写以下程式:

Private Sub Command1_Click()

Dim X(1 To 10) As String, N As Integer

Parse "AAA BBB CCC", X, N

End Sub

2. 假设 Parse 副程式是正确的,所以执行「Parse "AAA BBB CCC", X, N」之後,我们预期的结果是「X(1)="AAA"、X(2)="BBB"、 X(3)="CCC"、 N=3」,为了检验结果,首先将输入游标移到「Parse "AAA BBB CCC", X, N」的下一行叙述,然後按下 F9 将中断点设定在此一叙述之後。

3. 按下F5 执行程式,然後再按下表单上的命令钮,使程式执行过「Parse "AAA BBB CCC", X, N」,接着会进入中断模式,此时请检视区域变数视窗的 X 及 N,结果「X(1)="AAA"、X(2)="BBB"、 N=2」,与我们期望的不符合,这表示 Parse副程式是有错误的。

4. 要测试 Parse 副程式,采用「逐行执行」固然也可行,但比较节省时间的作法是将「Do」及「Loop Until pos = 0」两行叙述设定成中断点,因为我们可以在每次执行回圈之前先检视 Sin、 Sout、及 N 参数的内容,而每执行回圈一次,这几个参数的内容应该都会改变才对,所以也在「Loop Until pos = 0」叙述上面设定中断点,以检验结果。

5. 设定好中断点之後,按下 F5 及命令钮执行程式,并且在每次中断时检查 Sin、Sout、及 N 变数的内容,然後再按下 F5 执行程式,结果各次检验的结果如下:

中断点 检验结果
进入 Do 时 Sin="AAA BBB CCC", N=0
第1次的 Loop Until Sin="BBB CCC", N=1, Sout(1)="AAA"
第2次的 Loop Until Sin="CCC", N=2, Sout(1)="AAA", Sout(2)="BBB"
第3次的 Loop Until Sin="CCC", N=2, Sout(1)="AAA", Sout(2)="BBB"

检验到第3次的 Loop Until 时,我们可以发现在 Sin 等於"CCC"(不含空白字元)时,程式并未将它设定给 Sout(3),所以 Parse 副程式中的 If-End If 叙述需修正如下:

If pos > 0 Then ' 如果含有空白字元

N = N + 1
Sout(N) = Left(Sin, pos - 1)
Sin = Trim(Mid(Sin, pos + 1))

Else ' 如果未含空白字元

N = N + 1
Sout(N) = Sin

End If

监看式与中断点的搭配

在上一个测试的例子中,不知道您会使用哪一种视窗来检验变数的内容,笔者使用的是监看视窗,因为区域变数视窗会列出所有的变数,看起来比较凌乱,使用监看视窗则是指定想要检视的变数,相对之下就清爽多了。承续上一个例子,假设我们想将 Parse 副程式的 Sin 变数加入於监看视窗中,则方法如下:

1. 选取功能表的「检视/监看视窗」,然後在「监看视窗」上面按下滑鼠右钮,待出现快显功能表时,选取「新增监看式」命令。

2. 待出现「新增监看式」交谈窗时,分别在「程序」栏位中选取「Parse」,在「运算式」栏位输入 Sin,则 Sin 变数即会成为被监看的变数。

使用监看式还有另一个特殊的功能,以下笔者直接以实例来说明,假设一回圈如下:

Dim A As Single

A = 0

Do

A = A + 0.01

Loop Until A = 1

此一程式会进入无穷回圈,假设我们不清楚为什麽,所以想利用VB侦错的功能加以检验,此时如果我们将中断点设定在「Loop Until A=1」,则为了让 A 的值接近於 1 以检验为什麽「A = 1」不成立,所需按下 F5 持续执行程式的次数大约是100次,这好像太辛苦了一点,所以便有了以下的方法:

1. 在「新增监看式」交谈窗中,输入「A > 0.95」到「运算式」栏位中,然後选取「监看方式」栏位中的「数值由 False改变为 True时中断」,如图-5,此一设定的作用是当 A > 0.95时,使程式进入中断模式。

图-5 条件式中断设定法

2. 除了新增「A > 0.95」的监看式之外,也增加变数 A 的监看式,然後按下F5执行程式,结果在 A=0.9599994时进入中断模式,您可以发现 A 竟然不等於 9.6,这是电脑在小数点方面的些微误差所致。

3. 接下来则利用 F8 逐行执行程式,果然发现 A 最接近1的两个值分别是 0.9999993 及 1.009999,并没有出现等於 1 的数值,这是以上回圈会进入无穷回圈的原因。

检视程序呼叫的历程

对於比较复杂的程式而言,程序连续向下呼叫的情况时有所见,当程式中断於某一程序时,我们往往也需要知道整个呼叫的过程中,是从哪一个程序开始呼叫的,经过哪些程序,执行过哪些叙述,如此方可正确地判断目前所呈现的结果是为何而来。

当呼叫历程超过两个程序以上,而且又进入中断模式时,VB 便允许我们选取功能表的「检视/呼叫堆叠」以显示程序的呼叫历程,举例来说,Command_Click 程序呼叫了 Insert副程式,而Insert 副程式又呼叫了 Swap 副程式,则当程式中断於 Swap 副程式时,则选取功能表的「检视/呼叫堆叠」,可显示如图-6 的「呼叫堆叠」视窗。

图-6 呼叫堆叠视窗

此时我们可以选取并且显示呼叫堆叠视窗中的任何一个程序,接着被选取的程序便会显示在程式视窗之中,并且以箭号标示出此一程序最後一个被执行的叙述,如果想要确知程式执行的经过,则只要逐一选取并且显示呼叫堆叠视窗中的每一个程序即可。

此一功能对於多模组程式的侦测很有帮助,因为即使是跨越不同模组的呼叫也都会显示在呼叫堆叠视窗中,而藉此我们可以浏览程序完整的呼叫过程,不必为了寻找不同程序而在各个模组之间切来切去。

使用 Debug 物件

以上所介绍的侦错功能都必须在中断模式进行,但也许我们觉得程式已经测试得差不多了,应该不会有 bug 了,於是不再中断程式,而让程式一路执行下去,并且期望它能够正确地执行,但往往事与愿违,结果又出现了bug,此时该怎麽办呢?当然必须重新设定中断点重新侦错,然而随着程式越写越大,必须设定中断点的地方也越来越多,而重新设定的工作也会越来越沈重,由此可见只使用以上的侦错功能仍然是不够的。

为了解决上述的问题,我们通常会在程式之中比较关键的地方埋设「侦测码」,举例来说,呼叫某一副程式之後,将呼叫的结果显示出来,以检测副程式是否正确执行,而当我们埋设很多侦测码之後,将来只要程式一执行,就不断会有阶段性的执行结果被显示出来。若程式执行正确,我们可以不去理会阶段性的执行结果,如果发生错误,才利用阶段性的执行结果清查首度出现错误的程式段,然後将侦错的火力集中在这段程式上面。

显示阶段性的执行结果,VB 程式常用的方法有二:MsgBox 及 Debug.Print。使用 MsgBox 的好处是会显示讯息窗,等我们看清楚结果後,才按下「确定」钮关闭讯息窗,但笔者并不鼓励您使用 MsgBox,因为它会影响视窗或控制元件的某些状态(主要是「作用中」及「非作用中」状态),进而引发其他的事件,至於 Debug.Print 则是把结果输出於即时运算视窗中,是笔者比较建议的方法。

使用 Debug.Print 好处多多,首先我们不必中断程式,只要开启即时运算视窗,就可以看到Debug.Print 输出的结果,此外,程式编译成执行档时它并不会被编译进来,不像大部分的程式语言,在程式最後编译成执行档之前,必须先拿掉侦测码。

从除错到0错误

最後笔者想提醒您一点,除去所有已知的错误并不表示程式就没有错误了,这就好像没看到蟑螂并不表示家里没有蟑螂了,如何写出没有错误的程式,又是另一个挑战性的课题,但这才是程式最後的目标,不是吗?