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

VB程式设计内功讲座(二 )

变数(物件)在程式中的活动模式

有时候觉得写程式的工作跟侦探差不多,因为在程式的开发过程中,总免不了会有bug发生,而程式设计者的责任就是把制造 bug 的元凶找出来。别以为当个侦探很好玩,除了必须绞尽脑汁找出问题之外,还要做到勿枉勿纵,如果程式是一组人开发出来的,有问题的时候,每个人都不希望问题出在自己所写的程式中,身为一个组长,必须要有足够的能力判断可能是元凶的程式是哪一个,然後要求组员进行检测,以求正确地找出问题的原因,否则没有问题的程式检测了半天,不仅劳民伤财,还会延误程式开发的商机。

程式错误的原因很多,但根本道理却在资料(变数或物件)上面。我们可以把程式简单地分成「程式码」(code)及「资料」(data)两部分,虽然两者都可能是造成错误的原因,但程式码的部分在执行阶段是死的,比较容易侦测,而资料的部分则会随着程式执行的状况来改变,是比较难掌握的部分,笔者作个比喻,某一大楼发生了窃案,那麽侦察的方向有二:(1)检查保全系统是否有漏洞:由於保全系统是固定的,所以比较容易检查,如果与程式作个比较,它像是程式码的部分,(2)嫌疑犯的调查:对於可能进出大楼的人员进行调查,但由於人的行为是自由的,所以这个部分的调查工作要比保全系统的检查来得困难,如果与程式作个比较,它像是资料的部分。

上一期我们介绍了变数的组成元素,藉以了解资料的基本特性,本文让我们继续探讨资料在程式中的活动模式,藉以在程式出状况时能够抓出造成程式错误的元凶。

 


本文大纲


变数的活动范围

记得有一种活动,是在湖里抓鸭子,别小看鸭子笨笨的,水中的鸭子其实是很难抓的,所以比较常用的技巧是将鸭子赶到角落,以缩小鸭子的活动范围,最样子就比较容易抓到鸭子了。变数的道理也高深不到哪里去,但如果我们连变数的活动范围都搞不清楚,那麽就别想抓到因为变数所产生的错误。

区域变数

区域变数(local variable)顾名思义就是指在某一个区域里面活动的变数,当程式进入此一区域时,区域中的变数便会诞生,但是当程式执行过此一区域时,变数即告消失,典型的例子是程序(副程式、函数、或事件程序)中的变数,例如:

Sub SubX()
    Dim x As Integer ' x 是 SubX 副程式区域内的变数
    x = x + 100
    Print x ' 每次都印出100
End Sub

以上面这个 SubX 副程式为例,每次呼叫进入时,变数x才会诞生,而当程式执行过 End Sub 叙述时 (也就是程式结束此一区域的执行时),变数x即告消失,因此每次当SubX 副程式再度被呼叫时,变数 x 的初值都等於 0。

区域变数由於活动范围仅局限於某一程序,因此只要这个程序不是写得太大,变数的变化情形就很容易掌握,也就很容易侦错了,这是程式设计中最常使用的变数类型。

全域变数

所谓全域变数,指的是所有区域皆可使用的变数,也就是说不受「程序」范围所限制的变数。在VB里面凡是撰写在程序之外的变数均属於全域变数,例如:

Dim x As Integer ' 全域变数,可供多个程序共用
Sub SubX()
    … ' 可以使用变数x
End Sub
Sub SubY()
    … ' 也可以使用变数x
End Sub

在VB里面,全域变数又可分成「模组私用」全域变数、「模组公用」全域变数、及「专案」全域变数,稍後笔者会有更一进步的说明。

静态变数

所谓静态变数,在VB里面是指利用 Static 保留字宣告的区域变数,例如:

Sub SubX()
    Static x As Integer ' 静态变数
    Dim y As Integer ' 区域变数
    x = x + 100
    y = y + 100
    Print x ' 印出值依序是 100、200、300…
    Print y ' 每一次都是印出 100
End Sub

以上的静态变数 x 与区域变数 y 一样,其活动范围都限定於 SubX 副程式之中,但每次进入 SubX 副程式时变数 y 的值都会归 0,而x则保有其原来之数值,所以 SubX 副程式中的 Print x 每次列印的数值都会累加100。

选择变数类型的基本原则

当我们决定使用某个变数时,该将它宣告成区域、静态、还是全域变数呢?请参考以下的基本原则:

1.是否要提供给多个程序共用,如果是,才将变数宣告成全域变数。

有些人可能会觉得将变数宣告成全域变数最方便,因为任何程序都可以使用,但是全域变数的缺点是不容易侦错,以抓鸭子的活动为例,鸭子的活动范围越大,就越难抓到,同样的,任何程序都可以使用的变数,万一其变数值与我们预期的结果不一样时(这当然是bug),就必须逐一对每一个程序进行侦错,无形中增加了程式侦错的困难度。

2.是否必须记录程式执行的状态,如果是,则必须变数宣告成静态变数,因为区域变数每次进入程序的值都会归0,所以无法一直纪录着程式的执行状态。

3. 如果不属於情况12, 则将变数宣告成区域变数。

Option Explicit:强制变数宣告

VB 允许我们在不必事先宣告变数之下,就使用变数,例如:

Dim X As Integer ' 事先宣告变数X
Y = X + 2 ' Y变数未事先宣告,也是对的

但这样的程式撰写习惯却可能引起一些不容易侦测的错误,例如:

For i = 1 to 10
    Arr(i) = Arr(j)+10
Next I

以上程式中的 Arr(j) 应该是 Arr(i) 才对,但由於 VB 允许未宣告而直接使用变数,以致 j 被视为合法的变数,但j在回圈中却一直等於0。

在不必事先宣告变数之下就使用变数,就好像变数随时随地都会冒起来,跑到我们的程式中,而从以上的例子中,我们发现这种作法其实是有缺点的,为了改善这个缺点,VB提供给我们另一种选择,那就是在所有程序之外加上 Option Explicit 的叙述,加上 Option Explicit 的作用是:「要求 VB 把未事先宣告的变数视为错误」,因此以上程式若修改成:

Option Explicit
Dim i As Integer ' 事先宣告i
For i = 1 to 10
    Arr(i) = Arr(j)+10
Next I

则由於 j 变数未事先宣告,会被VB视为错误,因此可在编译阶段提早被侦察出来。

模组与全域变数

对VB而言,一个专案可以含有多个模组,如果我们在某一个模组之中宣告了全域变数,那麽这个全域变数可以提供给该模组的所有程序使用,是无庸置疑的事,但是在多模组的专案中,我们则要考虑另一个问题:某一个模组的全域变数可以给其他模组使用吗?

模组私用全域变数

在表单模组或一般模组中,如果我们利用PrivateDim 保留字来宣告全域变数,例如:(注 :一般模组指的是利用功能表的「专案/新增模组」而加入於专案之中的模组,此类模组将来会以.bas 的副档名来储存)

Private x As Integer
Sub SubX()

End Sub

则此一变数为「模组私用」全域变数,也就是说,此一全域变数只有该模组的程序可以使用,其他模组则不可以使用。

模组公用全域变数

在表单模组中,如果我们利用Public保留字来宣告全域变数,例如:

Public x As Integer
Sub SubX()

End Sub

则此一变数为「模组公用」全域变数,也就是说,此一全域变数也可以给其他模组使用。但请注意,使用的语法必须在变数之前冠上表单名称,假设以上例子中的全域变数宣告在 Form1 之中,则以下是Form1 模组中的程序与Form2 模组中的程序,在使用变数x上的差异:

' Form1 模组
Sub SubX()
    x = x + 100 ' 像平常使用变数的方法一样
End Sub

 

' Form2 模组
Sub SubY()
    Form1.x = Form1.x + 100
End Sub

由於Form2 模组使用的是 Form1 模组的全域变数,所以必须在变数之前冠上「Form1.」。

专案全域变数

在一般模组中,如果我们利用Public 保留字来宣告全域变数,则此一变数为「专案」全域变数。所谓专案全域变数,指的是同一专案中,所有模组的所有程序均可使用的变数,因此以这种方式所宣告的变数其活动范围将扩及整个专案。

最後我们以一个实例来整理以上各种变数在程式中的活动范围,假设专案中有含有两个表单模组— Form1、 Form2 及一个一般模组 Module1,而这几个模组中的所宣告的变数如下:

Form1

Private A1
Dim A2
Public A3
Sub SubX1()
Dim A4
End Sub
Sub SubX2()
Dim A5
End Sub

Form2

Private B1
Dim B2
Public B3
Sub SubY1()
Dim B4
End Sub
Sub SubY2()
Dim B5
End Sub

Module1

Private C1
Dim C2
Public C3
Sub SubZ1()
Dim C4
End Sub
Sub SubZ2()
Dim C5
End Sub

则这些变数在几个副程式中的可使用性如下表 :(「??」符号表示可使用、「F1」表示必须冠上「Form1.」才可以使用、「F2」表示必须冠上「Form2.」才可以使用,空白者表示不可使用)

副程式 A1 A2 A3 A4 A5 B1 B2 B3 B4 B5 C1 C2 C3 C4 C5
SubX1 ?? ?? ?? ??       F2         ??    
SubX2 ?? ?? ??   ??     F2         ??    
SubY1     F1     ?? ?? ?? ??       ??    
SubY2     F1     ?? ?? ??   ??     ??    
SubZ1     F1         F2     ?? ?? ?? ??  
SubZ2     F1         F2     ?? ?? ??   ??

物件的活动范围

物件按性质可分成控制元件、表单物件、及一般物件叁种,其中一般物件与变数一样,可分成区域物件、静态物件、及全域物件叁种,活动范围也与变数一样,以下让笔者来说明控制元件与表单的活动范围。

控制元件的活动范围与表单完全相同,当表单被载入时,表单上的控制元件即会被载入,当表单被载出时,控制元件即会被载出,若与变数做比较,则与全域变数的活动范围完全相同。

由於表单中全域(静态)变数及控制元件的诞生(灭亡)取决於表单的载入(载出),因此我们必须特别注意表单载入与载出的时机。导致表单被载入的叙述有「Load 表单名」、「表单 .Show」、及「使用表单的属性、全域变数、控制元件」叙述;至於表单被载出的情况则有「Unload 表单名」叙述及「使用者关闭表单」。

当表单被载出系统时,全域变数的变数值会归零,表单及控制元件的属性值会还原成设计阶段时的设定值,因此如果我们想保留变数值及属性值,不要使用「Unload表单名」关闭表单,须使用「表单名.Hide」或「Form.Visible = False」隐藏表单。但笔者必须特别提醒您一点,如果有表单被隐藏,则程式结束前应该使用以下方法载出所有的表单:

For I = 0 To Forms.Count - 1

Unload Forms(I)

Next

否则表面上所有的表单已经被关闭,让人误以为程式结束了,而实际上却还有表单在系统之中。

变数与系统记忆体

由於变数执行时会占用记忆体,因此如何正确地使用变数,也会影响系统的运作,首先让我们思考一个问题:「当程式越写越大时,程式所使用的变数也越来越多,会不会演变成程式执行所需之记忆体超过系统记忆体,而不能执行」,例如我们就经常看到某些产品在包装盒上面会标明「至少须 16MB 记忆体」之类的字眼。

需要使用大量记忆体的软体通常是功能强大的知名软体,我们自己写的程式是不是也会出现越来越吃记忆体的情事呢?感觉应该不会,但实际不然,如果使用变数时不了解系统内部的基本运作,则即使是小程式也可能会把系统记忆体吃光。

全域变数与静态变数

就我们所讨论的叁种变数而言,全域变数及静态变数会在所属之模组被载入系统时,占据一块记忆体,参考以下程式:

Dim X(1024) As Integer ' X占有 2 KB 记忆体

Sub SubX()

Static Y(2048) As Integer ' X占有 4 KB记忆体

End Sub

如果以上程式附属於「一般模组」(会在程式被载入时,即载入系统),则程式被载入系统时,X及Y阵列将占据6 KB 的记忆体,而直到程式被载出时,这6KB的记忆体才会归还给系统;如果以上程式附属於「表单模组」,则必须等到表单被载入时,X及Y阵列才会占据 6 KB 的记忆体,而当表单被载出时,这 6KB 的记忆体即会归还给系统。

了解以上的基本特性之後,笔者想特别说明以下两种通病:

(1) 将所有的全域变数都放在「一般模组」中:的确,将共用的变数放在一般模组中最为简便,但由於一般模组中的变数会在程式被执行时即占用系统记忆体,而在程式结束时才归还记忆体,这使得变数在程式执行期间一定会占用记忆体,而减少了系统可运用之记忆体。要避免此一问题,宣告大型变数(例如 Dim Y(40960) As Integer会占用 80 KB)时,想一想被宣告的变数是不是只有某一「表单模组」需要使用,如果是,则将那些变数放在表单模组中,若是小型变数(例如 Dim X As Integer 只占用 2 bytes), 则可不必顾虑此一问题。

(2) 将其他模组可能使用的变数放在某一「表单模组」中,然後使用「Form名 .变数名」的格式来使用变数:此一方式并没有什麽错误,但请注意当表单被载出时,表单中所有变数的记忆体将会归还系统,而使得变数值全部归零,如果这些变数的用途是记录程式执行的状况,则在表单被载出时,将会失去记录的功效。

区域变数

当程式进入某一区域时,系统会为该区域的区域变数配置好记忆体,而程式执行过该区域时,变数所占用之记忆体又会归还给系统。

区域变数可能占用的记忆体有「堆叠」及「系统记忆体」两种,其中堆叠是程式载入时就配置给程式的(通常小於64KB),不管怎麽使用都不会影响系统记忆体,对於小型变数而言,使用的是堆叠中的记忆体,但如果是大型变数,则须占用系统记忆体。

由於区域变数所使用的记忆体在不使用时即归还系统,一般而言并不会出现耗尽系统记忆体的现象,须特别注意的是递回呼叫(例如副程式呼叫自己,或者副程式A呼叫副程式B,而副程式B又呼叫回副程式A),如果程式没有设计好递回呼叫的出口,则副程式会一直呼叫下去,则位於该副程式中的变数将会持续地占用记忆体,而可能用尽堆叠或系统记忆体。

物件与系统记忆体

物件与变数最大的差异如图-1:

图-1 变数与物件的比较

比较特别的是物件的「具体物件」部分,当我们宣告某一物件变数时(例如 Dim X As Object),具体物件还不存在,直到执行建立物件的叙述之後,物件才会含有具体物件(详阅上一期VB专栏)。

若撇开具体物件的部分,物件与变数在记忆体的使用上是完全相同的,所以我们可以把重心放在具体物件上面,具体物件所占用的记忆体通常会跟物件变数一起被归还给系统,例如:

Sub SubX()
    Dim X As Object
    Set X = New 物件类别名

    …
End Sub

则程式执行到 End Sub 时,X所占用的记忆体会被释放,而属於X的具体物件也会一并被释放。

但并非每一种物件的具体物件都具有以上特性,有些物件的作法是,如果程式要释放具体物件,一定要呼叫释放物件的方法,如果程式忘了这麽做,则即使程式结束执行了,该具体物件依然会占据系统记忆体,还好的是此类物件并不多见,笔者提出此一状况,只是想提醒您一点,当我们使用某一种新的物件的,须查阅该物件的说明文件以了解该物件是否为此类物件。

变数与参数传递

当我们将一个大程式分成多个模组或程序之後,为了让不同程序之间能够共享资料,除了全域变数的使用之外,另一个方法则是程序呼叫时的参数传递。诚如我们前面在「选择变数类型的基本原则」段落中的说明,全域变数少用为宜,因此参数的传递在程式设计中就益形重要。

在VB里面,程序之间的参数传递有两种方式:「传值呼叫」(call by value)及「传址呼叫」(call by address),这两种参数传递方式在表面上很容易让人忽略其差异性,但实际上却会影响程式执行的结果,以下先以一个例子来说明这个问题,假设有一副程式定义如下:

Sub AddOne( x )
    x = x + 1
End Sub

而以下是两种不同的呼叫方式:

i = 10
Call AddOne( i )
Print i
i = 10
Call AddOne( (i) )
Print i

结果呼叫「Call AddOne(i)」之後,所得到的 i 值等於11,而呼叫「Call AddOne( (i) )」之後,所得到的i值却等於10。

注: 在以上的呼叫格式中,往往会省略 Call 保留字,如果省略 Call 保留字,则「Call AddOne(i)」要写成「AddOne i」(去掉参数周围的左右括弧),而「Call AddOne((i))」则写成「AddOne (i)」。

上述例子会有不同的输出结果,是因为参数传递方式有所不同的关系,以下让笔者来解说上述两种参数传递的差异及工作原理。

实际参数与形式参数

当我们定义副程式时,例如前面的 Sub AddOne( x ),由於我们并不知道将来呼叫者会传入哪些资料作为参数,因此只好给这个参数取一个暂时性的名称,例如x,而将来副程式被呼叫时它们会被真正的资料 (常数值或变数)所取代,所以这些参数都只是形式上的,故称之为「形式参数」(formal parameter)。

而呼叫程式端在呼叫副程式时,必须以实际的资料来替代形式上的参数,使得副程式能够拿到真正的资料来运算,这真正的资料就叫做「实际参数」(actual parameter),例如 Call AddOne( i )中的i。

 

所谓参数传递的方式,就是在呼叫副程式的过程中,编译器(或解译器)如何以实际参数来替代形式参数的方法,而方法不同,得到的结果也可能不同,解说参数的传递方式以前,让我们先以「变数的四个组成元素」来表示实际参数及形式参数的关系,就拿前面程式为例吧:

 

图-2 所谓传递参数,传的到底是名称、资料型别、位址、还是值呢?

就图-2来看,变数有四个元素,所谓传递参数,传的到底是名称、资料型别、位址、还是值呢?

其实除了传递「资料型别」无法达到传递参数的目的之外,其他叁个元素都可以作为参数传递的标的物,不过VB 并不支援传递「名称」的方式,以下笔者就针对传「值」及传「位址」两种方式来加以解说。

传值呼叫

这种传递参数的方式,是把呼叫者的实际参数值,复制一份到副程式的形式参数位址内,尔後副程式不管怎麽对形式参数进行运算,完全不会对实际参数的变数值有所影响。

 

图-3 传值呼叫

以前面的例子来说,呼叫程式端在实际参数的前後加上 (),例如「Call AddOne( (i) )」,其传递方式就是传值呼叫,所以以下的呼叫:

i = 10
Call AddOne( (i) )
Print i

变数 i 的值不会因执行了 AddOne() 而被改变,因此输出结果等於10。

传位址呼叫

所谓传值呼叫,我们可以把它想成「以实际参数的值取代形式参数的值」,而传位址呼叫则是「以实际参数的位址取代形式参数的位址」,示意图如图-4:

 

图-4 传址呼叫

从图-4来看,我们可以把传位址呼叫中的实际参数与形式参数想成「同一个变数」(名称虽不同,但实际所占用的是同一块记忆体)。

以前面的例子来说,Call AddOne( i ) 由於未在变数 i 的前後再加上(),被视为传位址呼叫,因此呼叫之後的i值将因为形式参数x(与i为同一变数)的改变而跟着改变,所以以下的叙述:

i = 10
Call AddOne( i )
Print i

i 的输出结果等於11。

ByVal 保留字与传值呼叫

在呼叫程式端,利用左右括弧将参数框起来,可使参数传递成为传值呼叫,此外,我们也可以在副程式(函数)定义端,利用ByVal 保留字将参数传递设定成传值呼叫,例如副程式定义如下:

Sub AddOne( ByVal x )
    x = x + 1
End Sub

则不管「Call AddOne( (i) )」或是「Call AddOne( i )」均被视为传值呼叫。

传值呼叫的限制

在VB里面,「阵列」的传递是不可以采用传值呼叫的,例如以下的呼叫会产生错误:

' SubX 副程式的定义
Sub SubX( data() As Integer ) ' data() 代表一阵列参数
    …
End Sub

' 呼叫 SubX 副程式
Dim X(100) As Integer
Call SubX( X ) ' 采传址呼叫,正确
Call SubX( (X) ) ' 采传值呼叫,VB不接受

传递阵列时,VB 之所以不接受传值呼叫的理由是,传值呼叫必须「复制」整个阵列,这对於大型阵列(例如含有30000个阵列元素),是一件比较不符合效率的事情,反之,若采用传址呼叫,以上面的程式为例,只要将 X 阵列的位址设定给 data 参数即可,而不必大量复制阵列的资料。

传址呼叫的限制

就像传值呼叫有其限制一样,传址呼叫也有其限制。传址呼叫的限制是「不同资料型别的实际参数不可作为传递的参数」,例如:

' SubX 副程式的定义
Sub SubX( I As Integer ) ' 参数宣告成 Integer
    …
End Sub

' 呼叫 SubX 副程式
Dim L As Long
Call SubX( L ) ' 采传址呼叫

为什麽不接受呢?请参考图-5:

 

图-5 传址呼叫不接受不同型别的资料传递

假设「Call SubX( L )」是正确的,那麽将会造成:「Integer型别的变数I,其位址却指向一块型别为Long 的记忆体」,这显然违反了程式运作的原则,所以会产生错误。

不过,如果我们在副程式定义端,将形式参数宣告成Variant(不定型)或者不宣告资料型别,则该参数可以接受任何型别的资料,因为Variant的资料型别(及不定型型别)可随着资料来改变,如图-6:

 

图-6 传址呼叫不接受不同型别的资料传递