第06章 模块化程序设计

更新时间:2024-05-31 03:44:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

第6章 模块化程序设计

本章内容提示:VB应用程序是由一系列过程组成的,前面各章所涉及的例题和习题,除了定义一些公共的常量、变量或数组以外,编写的代码都写在事件过程中。而实际应用中,往往要根据问题的复杂程度,按照结构化程序设计的思想,将应用程序按功能划分为若干个模块,每个模块还可以继续细分为子模块,每个子模块完成具体的任务,模块和子模块均是可被重复调用的程序段,由编写人员按照一定的格式建立,称为用户自定义过程(本书简称过程)。VB中的过程分为Function过程和Sub过程。

教学基本要求:了解应用程序设计中引入过程的目的和意义;掌握过程定义、调用方法以及过程调用过程中参数传递的形式和特点;掌握变量、过程作用域及其对程序运行结果的影响;了解过程的递归调用;培养学生模块化程序设计思想。

6.1 模块化程序设计思想概述

所谓模块化设计,是指在程序设计中将一个复杂的算法系统分解成若干相对独立、功能单一的模块,并利用这些模块积木式地组合成所需的全部程序。采用模块化思想设计的程序系统具有以下三个特点:第一,由于模块间是相互独立的,所以每个模块可以独立地被理解、编写、测试、排错和修改,这就使得程序容易设计,也容易理解和阅读;第二,模块的独立性也能有效地防止错误在模块之间扩散蔓延,因而有助于提高软件的可靠性;第三,模块化由于能够分割功能而接口可以简化,因此,可由许多人分工合作开发复杂大型软件,有助于软件开发工程的组织管理。

VB中将上述具有独立功能的模块称为过程,其分为两类:Sub过程和Function过程。下面将分别进行讨论。

6.2 Sub过程

VB中有两类Sub过程:一类是事件过程,每一个事件过程都对应一个Sub过程。事件过程由VB系统定义,用户仅编写实现具体功能的代码,当该事件发生后,系统自动调用事件过程,而相应的过程代码被执行。前几章编写的代码基本上都是写在某个事件过程中。另一类就是本节要学习的内容,该类过程由用户定义,实现具体功能的代码也由用户编写,可供事件过程或其他过程调用。

6.2.1 Sub过程的定义

Sub过程的建立有两种方法,第一种方法是通过菜单建立,第二种方法是在代码窗口下直接建立。这里介绍后一种方法。

在窗体的通用声明段或标准模块的代码窗口中,直接输入Sub过程,格式如下: [Static] [Private|Public] Sub 过程名([参数列表])

语句组 End Sub 其中:

(1) 过程名 为过程的标识符,其命名规则与变量相同。

(2) ([参数列表]) 表示执行Sub过程所需要的参数。该类参数本身没有值,只代表参数的个数、位置和类型,只有在被调用时才有确定的值,因此也称为形式参数(简称形参)。

-139-

对形参的具体要求是:形参只能为变量(除定长字符型变量外)和数组,不得为常数或表达式;形参可以有多个,参数之间用逗号隔开。

在过程定义时,当省略[参数列表]时,该类过程称为无参过程,但过程名后的圆括号不得省略。

(3) [Private|Public] 用于说明过程的作用域,[Static]用于确定过程变量为静态变量,将在本章第4节中详细介绍。

(4) 语句组 又称为过程体,用来实现过程功能的代码。 如果过程体中含有Exit Sub 语句,表示强行退出过程。

6.2.2 Sub过程的调用

Sub过程调用有两种格式:

第1种格式:Call 过程名 [(参数列表)] 第2种格式:过程名 参数列表 其中:

(1) 参数列表 代表要传送给Sub过程的实际值,称为实际参数(简称实参),可以为常量、变量、数组元素、数组名或表达式等形式。与形参类似,参数之间用逗号分隔。

(2) 在调用Sub过程时,实参和形参按它们的位置建立一一对应关系,实参的值或地址传给对应位置上的形参后,执行过程体,当遇到End Sub或Exit Sub语句时,结束Sub过程,并返回主程序(调用过程语句所在的程序称为主程序)。 例6-1 求组合数Cn?mn!的值,设m=6,n=10。

m!(n?m)! 分析:本题需要计算不同数的阶乘3次,可以编写一个Sub过程,求任意整型数x的阶乘,以供主程序中多次调用。定义过程时需要设置2个形参,一个用于传入x值,另一个用于存放计算结果。程序代码如下:

Sub fact(x As Integer, f As Double) Dim i As Integer

f = 1 'B For i = 1 To x f = f * i Next i

End Sub 'C Private Sub Command1_Click()

Dim m As Integer, n As Integer, s As Double, y As Double n = 10: Call fact(n, y) 'A s = y 'D m = 6: Call fact(m, y) 'E s = s / y 'F Call fact(n - m, y) 'G s = s / y 'H Print \End Sub

为了便于描述程序的运行过程,程序中注释字符用于标记程序执行的位置。 程序执行过程描述如下:

(1)当单击窗体上命令按钮(Command1),程序运行Command1_Click事件,声明变

-140-

量后,变量获得初值为0。

(2)程序运行到A处,调用fact过程,通过参数传递将实参n、y的地址传给过程形参x、f,使得n与x、y与f分别共用同一存储区域,在过程中对形参x、f的操作也就是对实参n、y的操作。

(3)程序运行到B处,f获得值为1,开始进行阶乘运算。

(4)程序运行到C处,f中保存的就是n(本次n=10)的阶乘值,返回主程序D处。 (5)程序运行到D处,此时的y就是10!,转存到变量s中。

(6)程序运行到E处,再次调用fact过程,将实参m、y的地址传给过程形参x、f(注意,这时m的值为6,f的值仍为10!)。

(7)程序再次运行到B处,f原来的值被1取代,开始进行6的阶乘运算。 (8)程序运行到F处,将6!计算到变量s中。

(9)程序运行到G处,第三次调用fact过程,计算(10-6)!。 (10)程序运行到H处,将4!计算到变量s中,最后输出结果。

通过上例,初步了解了Sub过程的定义、调用方法、程序执行流程,到底过程调用时实参与形参之间的数据传递是如何完成的,学习了下节就一目了然了。

6.2.3 过程调用中的参数传递

形参与实参之间的数据传递作用可以简单理解为:为过程传递运算对象和将过程执行结果返回给主程序的“桥梁”。在过程被调用之前,所有形参只是起到标识运算对象“模板”的作用,当程序流程转去执行过程时,实参按一定方式将数据传给形参后过程体被执行,过程的运算结果还可通过形参将数据传给实参返回到主程序。过程调用中参数传递有两种方式:传值和传地址,默认为传地址。 1.传地址方式

传地址是VB默认的参数传递方式。在这种方式下,实参传给形参的是存储地址,使得形参与实参共用同一存储单元,因此,在过程中对形参的任何操作实质都是对相应实参的操作。

在程序设计中,利用传地址方式可以获得过程处理的结果。要实现传地址方式可在过程定义时对形参作标识或在过程调用时对实参作限制,具体办法是:

(1) 在过程定义时,形参前加ByRef显式说明(省略也可以); (2) 在过程调用时,与形参对应位置的实参必须是相同类型的变量或数组名,实参为常数或表达式是无法实现地址传递的。参数传递时,当实参为变量时,把实参的地址传递给形参,使实参与形参共享同一存储单元段;当实参为数组时,把实参数组的存储地址传递给形参数组,使实参数组与形参数组共享同一存储区域。

注意:编写过程定义时,形参中数组名只须带括号,不指定元素个数;调用过程时,实参数组名既无括号,也不能指定元素个数。

对于例6-1而言,过程的两个参数x和f均为地址传递。

例6-2 一个数组有10个整数元素,将第一个元素与最后一个元素对调,第二个与倒数第二个对调……,输出对调前后数组各元素的值。

根据题意可知,需要两次输出数组各元素的值,可以定义一个过程供主程序中调用,其作用为输出任意一个一维整型数组。过程定义和过程调用实现方法如下: 新建一个工程,在窗体的通用声明段中,定义过程parray:

Option Base 1

Sub parray(a() As Integer) Dim p

-141-

For each p in a Print p; Next p Print End Sub

在窗体上添加一个命令按钮,Command1的单击事件下调用上述过程,代码如下: Private Sub Command1_Click()

Dim x(1 To 10) As Integer, i%, t% For i = 1 To 10

x(i) = Int(Rnd * 100) Next i

Print \交换前各元素值:\

Call parray(x) '调用过程,将数组x作为参数传给形参,输出数组 For i = 1 To 5

t = x(i): x(i) = x(10 - i + 1): x(10 - i + 1) = t Next i

Print \交换后各元素值:\

Call parray(x) '再次调用过程,将数组x作为参数传给形参,输出数组 End Sub

运行程序后,单击Command1,Command1_Click()事件过程代码被执行,首先定义数组并为数组元素赋值后,执行调用过程语句Call parray(x),第1次调用parray过程,输出交换前数组各元素的值;程序流程返回到Command1_Click()中Call parray(x)的后续语句FOR语句,执行循环,完成数组元素的交换后,执行Next i的后续语句Call parray(x),第2次调用parray过程,输出交换后数组各元素的值;程序流程返回到Command1_Click()中Call parray(x)的后续语句End Sub。

需要说明的是:过程parray中的形参数组a是任何一个一维、整型数组的“模板”,本身没有实际值。Command1_Click()中定义的数组x是具有10个元素的一维整型数组;它为主程序并使用Call parray(x)调用Sub过程;调用时将x数组的存储地址传给形参数组a,过程中对形参数组a进行输出,实际上就是对实参数组x输出。

例6-3 编写求两个整数的最大公约数过程,在主程序中调用该过程求两个数的最大公约数,并根据最大公约数求最小公倍数。

分析:要求两个整数的最大公约数,需要在过程定义中设置2个形参用于接收这两个整数,再设置1个参数用于存放过程中得到的最大公约数。代码如下:

Sub gys(m As Integer, n As Integer, t As Integer) Dim r As Integer Do

r = m Mod n

If r = 0 Then Exit Do m = n n = r Loop t = n End Sub

在Command1_Click单击调用gys,代码如下:

-142-

Private Sub Command1_Click()

Dim a As Integer, b As Integer, x As Integer, y As Integer a = 16: b = 12

Call gys(a, b, x) y = a * b / x

Print \最大公约数为:\ Print \最小公倍数为:\End Sub

运行结果“最大公约数为:4”,“最小公倍数为:12”。 这个结果显然不对,为什么呢?下面进行分析:

在本例中,过程gys定义时形参个数为3个,其中前2个形参用于接受运算对象,第3个形参用于返回运算结果,所有的形参前均省略了关键字ByRef;过程调用时实参为变量,因此在过程调用时,参数传递方式为传地址方式,过程中对形参m、n和t的处理,实际上就是对实参a、b和x的处理。过程中m、n的值发生了变化,就造成了调用过程前a和b的值与调用过程后a和b的值是不相同的。根据最大公约数和调用过程结束后的a、b值,计算得到的最小公倍数是错误的。

如何使形参改变了的值不会影响对应位置的实参呢?VB提供了另一种传递参数的方式——传值方式。 2.传值方式

当以传值方式调用一个过程时,实参将其值复制给形参后,就失去与形参的“联系”,此时形参拥有独立的存储单元,过程执行中如果形参的值发生变化,对应位置的实参值不会受任何影响。当过程调用结束时,形参所占用的存储单元也同时被释放。

实现传值方式也可体现在过程调用和过程声明中。

(1)在过程调用时,如果实参为常量、表达式或为带括号的变量,参数传递是按传值方式进行的。

在例6-3中,如果将调用过程语句改为Call gys((a), (b),x),就可以得到正确的结果。这是因为在过程调用时,实参a、b与形参m、n之间的传递方式是传值,只有实参x与形参t传递方式是传地址。过程gys中,对a和b的改变不会影响对应位置的实参a、b,而只有对t的改变会传递给对应位置的实参x。

(2)定义过程时,如果形参前加ByVal关键字。调用过程时,不管实参以何种形式,参数传递均采用传值方式。

在例6-3中,只要将过程定义的第一行改为:

Sub gys(ByVal m As Integer, ByVal n As Integer, t As Integer)

调用该过程时,无论实参是何种形式,形参m和n接受的是实参的值,过程调用时参数传递方式为传值方式,过程中m和n值的改变都不会影响相应位置的实参。

例6-4 分别用传地址方式和传值方式编写交换两个整数的过程并调用。

Sub swap1(ByVal x As Integer, ByVal y As Integer) Dim t As Integer t = x: x = y: y = t End Sub

Sub swap2(x As Integer, y As Integer) Dim t As Integer t = x: x = y: y = t End Sub

-143-

Private Sub Command1_Click() Dim a As Integer, b As Integer a = 3: b = 4

swap1 a, b ‘调用传值方式过程 Print \ a = 5: b = 6

swap2 a, b ‘调用传地址方式过程 Print \End Sub

运行程序可以看到,过程swap1不能实现变量值的交换,这是因为它采用的是传值方式,过程中交换的是形参x和y的值,交换的结果不会影响实参a和b;而过程swap2采用的是传地址方式,形参与实参共用同一存储单元,过程中对形参的交换实际上就是在交换实参,所以可以完成对两个变量值的交换。

使用过程编写程序时,初学者往往思想比较混乱,总觉得无从下手,为此,建议如下: 首先,定义过程时,应根据处理问题的需要,确定形参的个数及其作用,明确参数传递方式,以确定对形参与实参具体要求。在形参前加ByVal和ByRef(或省略ByRef),确定形参的类型和作用。

其次,调用过程时,要根据形参个数、数据类型及参数传递方式,确定实参个数与类型。 在选择参数传递方式时,遵照如下原则:

(1)过程中处理的对象是数组时,只能采用传地址方式。

(2)过程的运算结果需要通过参数返回时,必须采用传地址方式。 应用上述思想,将求两个数的最大公约数gys过程定义为: Sub gys(ByVal m As Integer, ByVal n As Integer, t As Integer) 有如下优点:

第一,过程中各参数作用明确,形参m和n接受的是实参的值,形参t可以返回过程运算结果,参数传递方式分别是传值和传地址。

第二,调用过程时,格式简单,实参不需要加括号。如,调用gys过程语句Call gys(a, b, x),无论实参a、b带括号与否,均不会出现错误的。

第三,调用过程时,对实参的数据类型可稍宽松。在传值方式下,实参与形参数据类型只要相容图6-1 实参与形参类型不一致时的出错信息 即可。比如,若形参为双精度时,实参可以是任何

数值型数据。而在传地址方式时,实参的类型必须与形参一致,否则会出现如图6-1所示错误。

例6-5 编写求一组整数平均值的过程,并在主程序中调用。 分析:编写过程代码时,首先考虑形参个数及参数传递方式。因为要处理的是一组整数,个数并没有确定,所以设置一个整型形参数组接受处理对象。而运算结果只有一个平均值,所以设置1个普通变量返回(平均值),参数传递方式均为传地址方式。过程调用时,实参的个数应与形参个数一样,第一个实参应为数组,第二个实参只能为变量。 实现求一组整数平均值的过程如下:

Sub Tj(x() As Integer, aver As Single)

Dim m As Integer, n As Integer, i As Integer, s As Integer m = LBound(x)

-144-

n = UBound(x) For i = m To n s = s + x(i) Next i

aver = s / (n - m + 1) End Sub

调用上述过程的代码为: Private Sub Command1_Click() Dim a%(1 To 10), i%, aver! For i = 1 To 10

a(i) = Int(Rnd() * 10) '数组元素值随机产生,以方便读者调试程序 Print a(i); Next i Print

Call Tj(a, aver) '调用过程,获得平均值 Print \这些数的平均值为:\End Sub

如果需要从过程中获得多个处理结果,则需要设置多个参数。例如:

例6-6 编写能获得一组整数的平均值、最大值和最小值的过程并调用。代码如下: Sub Tj(x%(), max%, min%, aver!) Dim m%, n%, i%, s% m = LBound(x) n = UBound(x)

max = x(m): min = x(m): s = x(m) For i = m+1 To n

If max < x(i) Then max = x(i) If min > x(i) Then min = x(i) s = s + x(i) Next i

aver = s / (n - m + 1) End Sub

Private Sub Command1_Click()

Dim a%(1 To 10), i%, x%, y%, z! For i = 1 To 10

a(i) = Int(Rnd() * 10) Print a(i); Next i Print

Call Tj(a, x, y, z) '用x,y,z分别去对应形参的max,min,aver Print \这组数的最大值为:\ Print \这组数的最小值为:\ Print \这组数的平均值为:\End Sub

-145-

6.3 Function过程

如果过程只需要返回一个值,如计算N!及求一组数的平均值,使用VB提供了Function(函数)过程,可以使定义和调用都更加简便。Function过程不仅可以和Sub过程一样通过参数传递返回过程处理结果,还可以通过过程名返回一个处理结果。

6.3.1 Function过程的定义

定义Fucntion过程格式为:

[Static] [Private|Public] Function 过程名([参数列表]) [As 数据类型]

语句组 End Function 说明:

(1)与Sub过程相比,Function过程的过程名不仅标识函数过程,而且还有返回函数运算结果的功能,所以比Sub过程多了数据类型声明。

(2)语句组中一般应有一条语句将过程的运算结果赋给过程名。格式为:

过程名=表达式

(3)如果在过程体中含有Exit Function 语句时,表示强行退出过程。

6.3.2 Function过程的调用

Function过程一经定义,调用方式就与系统提供的内部函数完全相同。 例6-7 求组合数Cn?mn!的值,设m=6,n=10。函数过程及调用代码如下:

m!(n?m)!Function fact(ByVal n As Integer) As Double Dim i As Integer, f As Double f = 1

For i = 1 To n f = f * i Next i

fact = f '将处理结果通过函数名返回 End Function

Private Sub Command1_Click() Dim s As Double

s = fact(10)/ fact(6)/ fact(4) '注意调用方式 Print \End Sub

比较例6-1可以看出,使用函数过程时,定义时因为函数名可以返回一个值,所以就可以少一个形式参数;调用时因为函数名就带着处理结果,所以可以像使用内部函数一样直接写入表达式中。

实际上,将过程定义为Sub过程还是Function过程没有必然的界限。可以这样考虑:使用Sub过程能实现的功能,也一定能使用Funtion过程实现,反之亦然。但一般情况下,如果不需要过程返回处理结果,或者需要返回多个处理结果,则选择Sub过程;如果需要返回的运算结果只有一个,则选择Funtion过程会更方便些。

如将例6-6中的Sub过程中的关键字“Sub”换成“Function”,程序其它部分可以不作任

-146-

何变动,同样可以得到正确结果。

6.4 过程和变量的作用域

前面所述的过程代码均写在某一个窗体中,这些过程代码均被保存在它所在的窗体文件中。VB还允许将用户自己编写的过程代码单独保存成一个文件(扩展名默认为bas),这类文件叫标准模块文件。一个VB的应用程序一般是由若干个窗体和标准模块文件组成,每一个窗体可由若干个事件过程和自定义过程组成,每一个标准模块也可由若干个自定义过程组成。

过程在工程中所处的位置及声明方式不同,调用的范围也不同。过程可被调用的范围称为过程的作用域。

同样,变量是过程代码中必不可少的数据载体,变量声明的方式及位置不同,可被访问的范围也不一样,变量可被访问的范围称为变量的作用域。

6.4.1 过程的作用域

过程的作用域分为模块级和全局级两种。 1.模块级过程

模块级过程是指在窗体或标准模块通用声明段定义的、用Private关键字限制的过程,这类过程只能被它所属的窗体或标准模块中的其它过程调用。

例如:在窗体1的通用声明段定义一个模块级过程fact,分别被窗体1下的Command1_Click()和Form_Click()所调用是允许的。若在窗体2下调用窗体1中定义的过程“fact”,会出现如图6-2所示的提示信息。

Private Function fact(ByVal n As Integer) As Double Dim p!, i% p = 1

For i = 1 To n p = p * i Next i fact = p End Function

在Command1_Click()下调用fact函数过程:

图6-2 调用无效过程时提示信息 Private Sub Command1_Click()

Dim s!, i% s = 0

For i = 1 To 4

s = s + fact((i)) Next i Print s End Sub

在Form _Click()下调用fact函数过程: Private Sub Form_Click() Dim i%, s! s = 0

For i = 3 To 6

-147-

s = s + fact(i) Next i Print s End Sub

2.全局级过程

在窗体或标准模块中定义的过程默认是全局的,也可用Pub1ic关键字显式声明。全局级过程可供该应用程序中所有窗体和所有标准模块中的过程调用,但根据过程所处的位置不同,其调用方式有所区别:

(1) 在窗体中定义的全局级过程,该窗体之外的其它过程要调用,必须在过程名前加该过程所在的窗体名。

例如:定义在Form1通用段的函数过程fact。

Public Function fact(ByVal n As Integer) As Double Dim p!, i% p = 1

For i = 1 To n p = p * i Next i fact = p End Function

该函数可以被Form1的所有过程调用,也可以被同工程的任何窗体的任何过程调用。但在其它窗体中调用窗体1的“fact”过程,计算5的阶乘,调用格式为:Form1.fact(5)。

(2) 在标准模块中定义的全局级过程,该工程的任何过程都可以直接调用。 例如:在标准模块中定义函数过程“fact1”。

Public Function fact1(ByVal n As Integer) As Double Dim p!, i% p = 1

For i = 1 To n p = p * i Next i Fact1 = p End Function

在Form1的Command1_Click()下调用fact1函数过程:

Private Sub Command1_Click() s = 0

For i = 1 To 4

s = s + fact1(i) Next i Print s End Sub

在Form2中的Form_Click()下调用fact1函数过程:

Private Sub Form_Click() s = fact1(5) Print s End Sub

-148-

若一个工程包含多个标准模块,且其中过程名不惟一,在调用时为了区分不同的过程,应在过程名前加标准模块名。下面将模块级过程和全局级过程在定义方式、调用方式等方面的不同进行总结,如表6-1所示。

表6-1 过程的作用域

过程作用域 定义方式 能否被本模块的其他过程调用 能够被本应用程序的其他模块调用

模块级(私有)

窗体模块

标准模块

全局级(公用)

窗体模块

标准模块

子过程名前加Private 子过程名前加Public

能 能 能

能,但必须在过程名前加窗体名

能,但过程名必须唯一,否则要加标准模块名

不能 不能

6.4.2 变量的作用域

变量的作用域可分为过程级、模块级和全局级。过程级和模块级常被称为私有级变量,而全局级也常被称为公有级变量。 1.过程级变量

过程级变量的作用范围限制在声明它的过程内部,只有该过程内部的代码才能访问或改变变量的值。该类变量通常用来存储过程中的临时数据,在过程内部使用Dim或Static关键字来声明变量。例如:

Dim a As integer,b As Single Static a As String

如果在过程中未说明而直接使用了某个变量,则该变量被默认为局部于该过程的过程级变量。

用Static声明的变量称为静态变量,该类变量在过程执行结束后一直存在,直到窗体关闭。而用Dim声明的变量只在过程执行时存在,退出过程后这类变量就会消失。请看下面的代码段:

Private Sub Form_Click() Dim i As Integer i = i + 1 Print i End Sub

每次单击窗体,窗体上均显示相同的数“1”。这是因为,过程每次运行时,为变量i分配存储空间,过程运行结束后,变量i所占用的存储空间被释放,再次运行时变量i重新被分配内存空间。

再看下面的代码段:

Private Sub Form_Click() Static i As Integer i = i + 1 Print i End Sub

每单击一次窗体,过程变量i累加1次,第n次运行i的值为“n”。原因是用Static定义的变量为静态变量,过程第一次运行时,为变量i分配存储空间,运行结束后,i所占用

-149-

的存储空间被保护起来,其值也被保留下来,再次运行时,变量i 还使用原来的存储空间,其值也是上一次保留下来的值,所以之后的运算也就是在上一次值的基础上进行的。 2.窗体(模块)级变量

窗体(模块)级变量的作用域限制在声明它的窗体(模块)中,该窗体(模块)中的所有过程均可访问该变量,其它窗体(模块)则不能。该类变量在窗体(模块)的通用段中用Private或Dim关键字声明。

例6-8 窗体级变量的作用范围示例(结果如图6-3所示)。

Dim a As Integer, b As Integer, c As Integer Sub prod() c = a * b

Print \子程序\End Sub

图6-3 窗体级变量作用范围

Sub sum() c = a + b

Print \子程序\End Sub

Private Sub Form_Click() a = 5: b = 3

Print Tab(16); \ Print \调用prod前\ Call prod

Print \调用prod后\

Print Tab(16); \ Print \调用sum前\ Call sum

Print \调用sum后\ Call sum End Sub

3.全局级变量

全局级变量在所有模块的所有过程都能访问,它的作用范围是整个应用程序,该类变量在模块的通用段中使用Public关键字声明。

例6-9 变量的作用范围综合示例(结果如图6-4所示)。

Public tt As Integer ' 声名全局变量tt

-150-

Private Sub test1()

tt = tt + 10 ' 全局变量tt Print tt ' 显示110 End Sub

Private Sub test2()

Dim tt As Integer ' 声名局部变量tt

tt = tt + 20 ' 局部变量tt,本过程无法访问全局变量tt Print tt ' 显示20 End Sub

Private Sub Form_Click()

tt = 100 ' 全局变量tt Print tt ' 显示100 Call test1

Print tt ' 显示110 Call test2 图6-4 变量作用范围示例 Print tt ' 显示110 End Sub

从运行结果可出看出:当变量名相同而作用域不同时,将优先访问作用域小的变量。 在定义变量时应将变量声明为哪一个级别呢?这主要取决于变量要在什么范围内使用。 (1) 如果变量只在某一个过程中使用,它的运算结果也不被其它过程再次使用,则可以声明为过程级变量。如本书中的大部分例题,采用的都是这种级别的变量。

(2) 如果变量将在同一窗体的多个过程中被用到,且彼此之间还有相互关系,则可以声明为窗体(模块)级变量。

(3) 如果变量将在多个窗体被用到,且彼此之间还有相互关系,则可以声明为全局变量。 建议除非必需,尽量使用作用域小的变量,因为大型程序的开发一般由多人合作完成,分工编写不同的模块。变量的局部化使合作者不必担心各模块中使用的变量是否同名而相互影响。

下面将不同作用域变量之间的区别总结如表6-2所示。

表6-2 变量的作用域

变量作用域 局部变量 模块级变量 全局变量

声明方式 Dim或Static

声明位置 在过程中

被本模块访问 不能 能 能

被其他模块访问 不能 不能

能,如果是在窗体模块中定义,调用时必须加上声明窗体对象的名称

Dim或Private 模块的通用声明段 Public

模块的通用声明段

6.5 应用举例

例6-10 编程对键盘上输入的任意个数排序。

分析:排序算法在第5章已经介绍过,这里回顾一下算法过程。 (1) 定义数组;

(2) 为数组元素赋值;

(3) 输出排序前的数组元素值;

(4) 选择一种排序算法对数组各元素排序;

-151-

(5) 输出排序后的数组元素值; (6) 结束。

在这个算法中输出数组元素值的程序段被执行了两次,不需要返回值,可以将其写成一个Sub过程。排序是对数组中元素进行了重新排列,因为数组是传地址的,在过程中对形参数组排好序实际上会直接反映在实参中,没有其它结果需要返回,所以也用Sub过程

在窗体上添加1个文本框,用于输入待排序的数据,数据之间用逗号分隔,1个图片框用于显示排序前的数组及排序后的数组,1个Option1控件数组,元素分别为Option1(0)、Option1(1),用于选择是按升序还是降序排序,它们被置于框架Frame1中,窗体界面如图6-5所示,各控件属性设置放在Form_Load事件中,排序代码放在Option1控件数组的DblClick事件中,程序运行结果如图6-6所示。

代码如下:

'输出一维数组的过程,因为过程不需要返回值,所以定义为Sub过程 Sub parray(x$()) '因为数组是通过split函数赋值的,数组必须是字符型 Dim i%

For i = LBound(x) To UBound(x) Picture1.Print x(i); \ Next i

Picture1.Print End Sub

图6-5 窗体界面 图6-6 程序运行结果

Sub sort(y$(), Byval p%)

'排序过程,参数P用于判断是升序还是降序 Dim i%, j%, k%, t%

For i = LBound(y) To UBound(y) - 1 k = i

For j = i + 1 To UBound(y) If p = 0 Then

If Val(y(k)) > Val(y(j)) Then k = j Else

If Val(y(k)) < Val(y(j)) Then k = j End If

-152-

Next j

t = y(k): y(k) = y(i): y(i) = t Next i End Sub

Private Sub Form_Load() '设置控件属性 Text1 = \

Frame1.Caption = \排序选择\ Option1(0).Caption = \升序\ Option1(0).Value = True Option1(1).Caption = \降序\End Sub

Private Sub Option1_DblClick(Index As Integer)

'对Option1控件数组的双击事件编程,由Index来决定是升序还是降序 Dim a() As String

a = Split(Text1.Text, \

Picture1.Print \排序前的数据:\

Call parray(a()) '调用一维数组输出过程 Call sort(a(), Index) '调用排序过程 Picture1.Print Option1(Index).Caption & \排序后的数据:\ Call parray(a()) End Sub

本例中定义了两个Sub过程,一个用于输出一维数组,一个用于排序,前者有一个数组参数,后者除了一个数组参数外,还有一个决定升降序的参数。

例6-11 判断一个整数是否是回文数。所谓回文数是这样的数,将这个数从左向右读和从右向左读值相等。如121就是回文数,345就不是回文数。

分析:判断回文数可以有很多方法,由于VB中可以自动进行数值与数字字符串的类型互换,所以这里可以将输入的数当成字符串来处理。

Function hw(ByVal x As String) As Boolean '只需判断是与非,因此定义为布尔型 Dim n%, i% n = Len(x)

For i = 1 To n \\ 2

If Mid(x, i, 1) <> Mid(x, n - i + 1, 1) Then hw = False Exit Function End If Next i hw = True End Function

Private Sub Command1_Click() Dim x$

x = InputBox(\请输入一个整数\ If hw(x) Then

MsgBox x & \是回文数\

-153-

Else

MsgBox x & \非回文数\ End If End Sub

例6-12 设计一个数值转换函数,能够将十进制整数转换成16进制以内的任意进制数。 分析:十进制数转换成n进制,常采用的方法是“除n取余,余数倒写”,当n大于9时,需要把大于9的余数转换成字母。为了方便转换,可以将余数0~9、A~F分别放在一个字符串数组中。

转换函数过程名为DecToN有两个参数,一个是待转换的十进制整数,一个是需要转换的进制。转换结果是一个字符串(即函数值为一个字符型),程序代码如下:

Function DecToN(ByVal x%, ByVal n%) As String Dim p() As String, y$, r%

p = Split(\ If n > 16 Then DecToN =\ Exit Function End If y = \ Do

r = x Mod n x = x \\ n y = p(r) & y Loop Until x = 0 DecToN = y End Function

Private Sub Command1_Click() Dim x%, n%, y$

x = InputBox(\请输入待转换的十进制整数!\数值转换\

n = InputBox(\请输入需要的进制,注意不要大于16\数值转换\ y= DecToN(x, n) If y<>\

MsgBox \转换结果:\

Else

MsgBox \对不起!本程序不能转换超过16的进制!\

End if

End Sub

6.5 过程的递归调用

简单地说,递归就是一个过程调用自己本身。VB的过程具有递归调用功能。许多问题都具有递归的特性,用递归调用来解决会非常方便。

例6-13 利用递归调用计算n!。

分析:根据阶乘的定义,求n的阶乘可以转换为求n*(n-1)!,利用过程递归来完成。

n!= 1 n=0 n*(n-1)! n>0 -154-

代码如下:

Function fact(n) As Double If n > 0 Then

fact = n * fact(n - 1) Else

fact = 1 End If

End Function

Private Sub Command1_Click() Dim n As Integer, m As Double n = Val(Text1.Text) If n < 0 Or n > 20 Then

MsgBox \非法数据\请输入0-20之间的整数\ Exit Sub End If m = fact(n)

Label1.Caption = m End Sub

说明:当n>0时,在过程fact中调用fact过程,参数为n-1,这种操作一直持续到n=0为止。下面以n=5为例,说明递归调用的过程。

递归级别 执行操作 1 fact(5) 2 fact(4) 3 fact(3) 4 fact(2) 5 fact(1) 6 返回1 fact(1) 7 返回2 fact(2) 8 返回6 fact(3) 9 返回24 fact(4) 10 返回120 fact(5)

要编写递归过程的关键是写出能构成递归的两个条件: (1)递归结束条件及结束时的值;

(2)能用递归形式表示,并且递归向结束条件发展。 例6-14 用递归求两个数的最大公约数。

分析求最大公约数的方法可以得到构成递归的两个条件:

n m Mod n=0

r=m mod n :gys(n,r) m Mod n<>0

gys(m,n)= 函数代码如下:

Function gys%(ByVal m%, ByVal n%) Dim r% r = m Mod n

-155-

If r = 0 Then gys = n Else

gys = gys(n, r) End If End Function

注意:递归算法设计简单,但消耗机器时间和占据的内存空间比非递归要大很多。

教学体会

使用VB编写应用软件时,提倡用“可视化的思想进行界面设计,结构化的思想进行功能实现”,本章讲述的过程就是将功能相对完整的程序段组织在一起,便于在程序中多处调用,既提高了程序段的共享,也便于整个程序的调试和维护,是结构化程序设计思想的体现。本章有些概念、程序组织的结构是全新的,教师讲授费劲,学生学习“吃不消”现象普遍存在,但这章又是本书的重点和难点,应在教学中引起足够的重视。在教学过程中,应注意以下问题:

(1)与事件过程对比,充分理解用户自定义过程在程序设计中的作用,掌握使用自定义过程后程序结构的变化。

(2)掌握Sub过程和Function过程的定义与调用格式,熟悉参数传递方式及其特点,具备正确设置过程参数及参数传递的能力。

(3)在应用编程时,对Sub过程和Function过程不用刻意区别,用Sub过程可以实现的问题,同样可以用Function过程实现,反之亦然。编写过程的关键是确定参数的个数及其作用,明确参数传递方式以确定对形参与实参的具体要求。

(4)变量与过程的作用域是规定变量能访问或过程能被调用的范围,通过实例熟练掌握并能灵活应用。

-156-

习题

一、选择题

1.VB中在模块的通用声明段用Dim X 声明的变量是 变量。 (A) 过程级 (B) 模块级 (C) 全局 (D) 静态 2.在Visual Basic应用程序中,以下描述正确的是 。 (A) 过程的定义可以嵌套,但过程的调用不能嵌套 (B) 过程的定义不可以嵌套,但过程的调用可以嵌套 (C) 过程的定义和过程的调用均可以嵌套 (D) 过程的定义和过程的调用均不可以嵌套

3.以下程序运行时,单击命令按钮得到的结果是 。

Sub subp(b() As Integer) For i = 1 To 4 b(i) = 2 * i Next i End Sub

Private Sub Command1_Click() Dim a(1 To 4) As Integer

a(1) = 5: a(2) = 6: a(3) = 7: a(4) = 8 subp a

For i = 1 To 4 Print a(i); Next i End Sub (A) 2 4 6 8 (B) 5 6 7 8 (C) 10 12 14 16 (D) 出错 4.假定有以下两个过程:

Sub s1(ByVal x As Integer, ByVal y As Integer) Dim t As Integer t = x : x = y : y = t End Sub

Sub s2(x As Integer, y As Integer) Dim t As Integer t = x : x = y : y = t End Sub

则以下说法中正确的是 。

(A) 调用过程S1可以实现交换两个变量的值的操作,S2不能实现 (B) 调用过程S2可以实现交换两个变量的值的操作,S1不能实现 (C) 调用过程S1和S2都可以实现交换两个变量的值的操作 (D) 调用过程S1和S2都不能实现交换两个变量的值的操作

5.在窗体上添加一个命令按钮Command1和两个名称分别为Label1和Label2的标签,在通用声明段声明变量X,并编写如下事件过程和Sub过程:

Private X As Integer

Private Sub Command1_Click() X = 5: y = 3

-157-

Call proc(X, y) Label1.Caption = X Label2.Caption = y End Sub

Sub proc(ByVal a As Integer, ByVal b As Integer) X = a * a y = b + b End Sub

程序运行后,单击命令按钮,则两个标签中显示的内容分别是 。 (A) 5和3 (B) 25和3 (C) 25和6 6.下列程序输出结果为 。

Private Sub Command1_Click() For i = 1 To 3 GetValue (i) Next i

Print GetValue(i) End Sub

Private Function GetValue(ByVal a As Integer) dim S As Integer S = S + a GetValue = S End Function A) 4 B) 5 C) 10 7.以下程序的运行结果是 。

Dim x As Integer, y As Integer, z As Integer Sub s2(a As Integer, ByVal b As Integer) a = 2 * a b = b + 2 End Sub

Private Sub Command1_Click() x = 4 y = 4

Call s2(x, y) Print x + y End Sub (A) 0 (B) 8 (C) 12 8.以下程序的运行结果是 。

Private Sub Form_Click() a = 1: b = 2

Print \ Call mult(a, b)

Print \End Sub

Sub mult(x, ByVal y)

-158-

(D) 5和6 D) 11 (D) 14

x = 2 * x y = 3 * y End Sub

(A) A=1 B=2 (B) A=1 B=2 (C) A=1 B=2 (D) A=1 B=2 A=1 B=2 A=1 B=2 A=2 B=6 A=2 B=2 9.假定有如下通用过程:

Public Sub fun(a(), ByVal x As Integer) For i = 1 To 5 x = x + a(i) Next i End Sub

在窗体上添加一个命令按钮和一个文本框,然后编写如下事件过程: Private Sub Command1_Click() Dim arr(5) As Variant For i = 1 To 5 arr(i) = i Next i n = 10

Call fun(arr(), n) Text1.Text = n End Sub

程序运行时,单击命令按钮,则文本框中显示内容是 。 (A) 10 (B) 15 (C) 25 (D) 24 10.以下程序段的运行结果是 。

Private Sub Form_Click() Dim nx% nx = 3

Call abcd(nx) Print nx End Sub

Public Sub abcd(n As Integer) n = n + 5 End Sub

(A) 3 (B) 5 (C) 8 (D) 10

11.一个工程中包含两个名称分别为Form1和Form2的窗体,一个名称为mdlfunc的标准模块。假定Form1,Form2和mdlfunc中分别建立了自定义过程,其定义格式为:

Form1中定义的过程: Private sub frmFunction1() … End Sub

Form2中定义的过程: Pubilc sub frmFunction2() … End Sub

-159-

Md1func中定义的过程: Public sub md1Function () … End Sub

在调用上述过程的程序时,若不指明窗体或模块名称,则以下叙述中正确的是 。 (A) 上述三个过程都可以在工程中的任何窗体或模块中被调用

(B) frmFunction2和md1Function过程能够在工程中各个窗体或模块中被调用 (C) 上述三个过程都只能在各自被定义的模块中调用

(D) 只有md1Function过程能够被工程中各个窗体或模块调用 12.以下程序段的运行结果是 。

Function abc(n As Integer) As Integer abc = n * 2 + 1 End Function

Private Sub Form_Click() Dim x As Integer x = abc(3) * abc(4) Print x End Sub

(A) 63 (B) 0 (C) 1 (D) 空 13.以下程序段的运行结果是 。

Private Sub Form_Click() Dim x As Integer x = 4 Print x; Call test(x) Print x End Sub

Public Sub test(ByVal i As Integer) i = i + 1 End Sub

(A) 4 6 (B) 4 4 (C) 4 5 (D) 5 4

14.要想从过程调用后通过参数返回两个结果,下面过程说明合法的是 。

(A) Sub f2(ByVal n%,ByVal m%) (B) Sub f1(n%,ByVal m%) (C) Sub f1(n%,m%) (D) Sub f1(ByVal n%,m%) 15.下面过程运行后显示的结果是 。

Public Sub F1(ByVal n%, m%) Private Sub Command1_Click() n=n Mod 10 Dim x%,y% m=m\\10 x=12:y=34 End Sub Call F1(x,y) Print x,y End Sub

(A) 2 34 (B) 12 34 (C) 2 3 (D) 12 3 16.下列叙述错误的是 。

(A) Sub过程可以递归调用

-160-

(B) Sub过程不可以由其过程名返回结果值 (C) 表达式中可以调用Function过程 (D) 表达式中可以调用Sub过程

17.以下关于过程及过程参数的描述中,错误的是 。

(A) 过程的参数可以是控件名称 (B) 过程的参数可以是窗体

(C) 只有函数过程能够将过程中处理的信息传回到调用的程序中 (D) 用数组作为过程的参数时,使用的是\传地址\方式

18.模块中采用以下方式定义的过程,能被其它模块调用的是 。

(A) Private Sub S1() (B) Public Sub S2() (C) Private Function F1() (D) 均不能被其它模块调用

19.为了在同一模块中的不同过程之间互相传递数据,下述方法中错误的是 。

(A) 利用全局变量

(B) 利用传地址方式的变量作为过程参数 (C) 利用静态变量 (D) 利用模块级变量

20.在窗体模块的声明段中声明变量时,不能使用的关键字是 。

(A) Private (B) Public (C) Dim (D) Static 21.以下叙述中错误的是 。

(A) 打开一个工程文件时,系统自动装入与该工程有关的窗体、标准模块等文件 (B) 保存Visual Basic程序时,应分别保存窗体文件及工程文件 (C) Visual Basic应用程序只能以解释方式执行 (D) 事件可以由用户引发,也可以由系统引发

22.在窗体上画一个名称为Command1的命令按钮,并编写如下程序:

Private Sub Command1_Click() Dim x As Integer Static y As Integer x=10 y=5

Call f1(x,y) Print x,y End Sub

Private Sub f1(ByRef x1 As Integer, ByVal y1 As Integer) x1=x1+2 y1=y1+2 End Sub

程序运行后,单击命令按钮,在窗体上显示的内容是 。 (A) 10 5 (B) 12 5 (C) 10 7 (D) 12 7

23.设一个工程由两个窗体组成,其名称分别为Form1和Form2,在Form1上有一个名称为Command1的命令按钮。窗体Form1的程序代码如下:

Private Sub Command1_Click() Dim a As Integer

-161-

a=10

Call g(Form2,a) End Sub

Private Sub g(f As Form,x As Integer) f.Show

f.Caption=IIf(x>10,\End Sub

运行以上程序,正确的结果是 。 (A) Form1的Caption属性值为\(B) Form2的Caption属性值为\(C) Form1的Caption属性值为\(D) Form2的Caption属性值为\二、简答题

1. Sub过程和Function过程的异同点是什么? 2. 值传递与地址传递特点是什么?如何选择?

3. 在VB中,形参若是数组,问在过程体内如何表示其数组的上、下界?

4. 在Form1窗体通用声明部分声明的变量,可否在Form2窗体中的过程被访问? 5. 为了使某变量在所有的窗体中都能使用,应在何处声明该变量? 6. 在同一模块、不同过程中声明的相同变量名,两者是否表示一个变量?有没有联系? 三、编程题

1.自定义一个与VB内部函数Abs功能完全相同的函数过程MyAbs,要求函数过程中不能调用VB内部函数Abs。

2.编写程序,求S=A!+B!+C!,阶乘的计算分别用Sub过程和Function过程两种方法实现。

3.编写函数过程Gdc求两个数的最大公约数。调用此函数试求1260、198、72三个数的最大公约数。

4.编写一个产生随机整数过程,输出n个指定范围的随机整数。 5.编写过程求M*M方阵两个对角线元素之和。

6.编写判断一个整数是否为素数的过程,并调用该过程输出100~200之间所有素数。 7.编程输出10000~99999之间的全部回文式素数。

8.有一个数列前两项为1,从第三项开始,每一项均为前两项之和,求这个数列的第20个数,用递归实现。

9.思考将本章所有Sub过程用Funtion过程如何实现,而Function过程又如何用Sub过程实现呢?

-162-

实习指导

1.实习目的

(1) 通过实习理解过程基本概念;

(2) 创建过程的作用、方法和过程调用方法; (3) 理解Sub过程和Function过程的异同;

(4) 掌握过程调用时参数传递的两种方式及特点; (5) 理解过程、变量的作用域;

(6) 具备使用过程编写简单程序的能力; (7) 理解递归的概念及编程方法特点。

2.实习内容

(1)验证教材所有例题,理解过程中形参的类型、作用,并将例1、3、5、6、10-14题中涉及到的过程,仿照下面给出的示例写出相关信息。

示例:下面给出求N!的Function过程和Sub过程,过程名分别为funfact和subfact。 Function过程代码为:

Function funfact(ByVal n As Integer) As Double Dim f#, i% f = 1

For i = 2 To n f = f * i Next i funfact = f End Function 相关信息描述如下:

过 程 名:funfact 类 型:函数过程

参数说明:参数n为值参数,类型为整型,因为只能为整型数求阶乘 传出结果:通过函数名funfact返回结果

结果类型:双精度型,因为双精度型表示的数的范围最大 Sub过程代码为:

Sub subfact(ByVal n As Integer, f As Double) Dim i% f = 1

For i = 2 To n f = f * i Next i End Sub

相关信息描述如下:

-163-

过 程 名:subfact 类 型:Sub过程

参数说明:参数N为值参,类型为整型,因为只能为整型数求阶乘

传出结果:通过形参f传出计算结果,调用时与之对应的实参应为同类型的变量 结果类型:双精度型,因为双精度型表示的数的范围最大 (2)完成教材中编程练习中的4、5、7题。

3.有关问题分析

本章是教学中的难点,教与学两方面均存在一定的困难,下面所分析的问题,有的是编程中遇到的,有的是上机实习中遇到的。

(1)使用Function过程还是Sub过程

过程是一个具有某种功能的独立程序段,可供程序多次调用。对于一个具体问题,既可以使用Function过程,也可以使用Sub过程。但Sub过程与Function过程还是有区别的,Sub过程的过程名仅标识过程本身;Function过程的过程名除了标识过程本身以外,还有返回值的作用,因此,若过程有一个返回值时,则习惯使用Function过程,并通过函数名返回函数值;若过程不需要返回值或返回多个值时,则使用Sub过程;返回值通过实参与形参的结合带回,当然也可通过Function过程名带回一个结果,其余通过实参与形参的结合带回。

(2)过程中形参的个数和传递方式确定 对初学者,若定义过程时在确定形参的个数和传递方式问题存在问题,可从如下方面考虑问题:

首先,理解形参和实参的作用。一方面,调用程序为Sub过程或Function过程通过实参传递实际处理对象;另一方面,Sub过程通过地址传递方式将结果传递给调用程序,Function过程通过地址传递方式或函数名将结果传递给调用程序。形参的个数和类型就是由上述两方面决定的。对初学者,往往喜欢把过程体中用到的所有变量全作为形参,这样就增加了调用者的负担和出错概率;也有的初学者全部省略了形参,则无法实现数据的传递,既不能从调用者得到初值,也无法将计算结果传递给调用者。

其次,理解参数传递的方式和特点。VB中形参与实参的结合有传值和传地址两种方式。数据传递按照地址方式传递。传值方式只能从调用程序向过程传入初值,但不能将结果传出;而地址传递即可传入又可传出。

最后,注意实现传值和传地址对形参和实参的要求。在定义过程时在形参前加ByVal关键字或过程调用时变量加圆括号,数据传递按照传值方式;如果在形参前加ByRef关键字或省略(默认)或实参是数组、自定义类型、对象变量等,参数传递只能是地址传递。

(3)实参和形参类型对应问题

第一:在地址传递方式时,调用过程实参与形参类型要一致。例如: 函数过程定义如下: Public Function f!(x!) f = x + x End Function 主调用程序如下:

Private Sub Command1_Click() Dim y% y = 3

-164-

Print f(y) End Sub

上例形参x是单精度型、实参y整型,程序运行时会显示“ByRef参数类型不符”的编译出错信息。

第二:在值传递时,若是数值型,则实参按形参的类型将值传递给形参。例如: 函数过程定义如下:

Public Function f!(ByVal x%) f = x + x End Function 主调用程序如下:

Private Sub Command1_Click() Dim y! y = 3.4 Print f(y) End Sub

程序运行后显示的结果是6。因为调用程序声明y的类型为单精度类型,对应位置上的形参x的类型为整型数据类型,实参y和形参x按照值传递方式,因此实参按形参的类型将值传递给形参,即y的值3.4传给x时,x接受的值为3。

(4)变量的生命周期

过程级动态变量,是在过程调用时分配变量的存储空间,当过程调用结束,回收分配的存储空间,也就是调用一次,初始化一次,变量不保存值;过程级静态变量,当过程调用结束后,其值还保留。

示例:一个窗体上有一个文本框和一个命令按钮,向文本框中每输入一个数据,再单击命令按钮后可将这些数累加起来。如果代码如下:

Private Sub Command1_Click() Dim s!, x! x = Text1.Text s = s + x

MsgBox \目前累加的结果是:\End Sub

则每次运行后得到的结果都只能是最后一次录入的那个数,修改程序,将“Dim s!, x!”改为“Static s!, x!”。

窗体级变量特点是:当窗体装入,分配该变量的存储空间,直到该窗体从内存卸掉,才回收该变量分配的存储空间。

-165-

本文来源:https://www.bwwdw.com/article/drz6.html

Top