返回目录:excel表格制作
第六章 程序结构
第一节 程序的三大结构
6.1.1 顺序结构
所谓顺序结构,就是要求计算机按照一定顺序的做某一件事情。例如,先将A1单元格的背景颜色改为红色,再将A1单元格的字体改为斜体,再将A1单元格的字体加粗。程序可以这样写:
Sub S()
Range(“A1”).Interior.Color=vbRed
Range(“A1”).Font.Italic=True
Range(“A1”).Font.Bold=True
End Sub
当需要若干次用到Range(A1)的时候,可以用With-End With语句将这一段简写,简写后的程序为:
Sub S()
With Range(“A1”)
.Intetior.Color=vbRed
.Font.Italic=True
.Font.Bold=True
End With
End Sub
计算机在碰到With的时候,会自动将With后面的关键字加入到下面所有的第一个”.”符号左边。
6.1.2 分支结构
所谓分支结构,就是需要计算机根据具体的情况,执行具体的任务。例如,要求计算机根据A1单元格中的数的大小将A1单元格背景色进行修改,如果大于15则修改为红色,否则不修改。可以写程序如下:
Sub F()
If Range(“A1”)>15 then Range(“A1”).Interior.Color=vbRed
End if
当然分支结构还可以更加复杂一点,比如说,分支结构嵌套分支结构,让计算机先根据A1单元格里面的数决定是否修改背景色,如果修改了背景色,则根据A1单元格中的字体决定是否把A1单元格中的数字增加。
Sub F1()
If Range(“A1”)>15 then
Range(“A1”).Interior.Color=vbRed
if Range(“A1”).Font.Italic=True then Range(“A1”)=Range(“A1”)+1
Else
End If
End Sub
6.1.3 循环结构
所谓循环结构,就是让计算机循环反复做同样一件事情或者做类似的事情,直到循环结束。循环结构是计算机编程语言的灵魂所在,使用VBA对数据进行统计和整理,主要运用循环结构。例如,让计算机把A列中所有大于10的数字一一找出来,背景色涂成红色。
Sub C()
Dim i As Integer, Rng As Range
Set Rng=Range(“A:A”)
i=1
While Rng(i)<>””
If Rng(i)>10 then Rng(i).Interior.Color=vbRed
i=i+1
Wend
End Sub
6.1.4 流程图
所谓的流程图,就是用一些符号来表示计算机执行命令的过程。一般地,用一个方框表示顺序执行过程,用一个菱形表示分支执行过程,用箭头表达命令执行的方向。流程图是为了让初学者更好地理解计算机程序如何执行,VBA语句如何运行,在实际过程中并不存在,编程时也没有必要画出来。
以上面的这个程序为例子,说明一下这个程序是如何执行的。
第一步,声明i是整形变量,把Rng变量设定为A列所有的单元格,这样的话Rng(i)就表示A列中第i个单元格了。
第二步,将i的数值设定为1 (即i=1)
第三步,判断第i个单元格是否为空,如果不是空,则进入循环,否则跳出循环
第四步,判断第i个单元格的数是否大于10,大于10则将这个单元格背景涂成红色。
第五步,把i的数值加上1,然后返回第三步。
第六步,循环结束,程序结束。
这个程序流程图如下:
图6.1 流程图
第二节 分支结构
6.2.1 分支结构的用处
分支结构用于需要判断的地方,例如在一般顺序结构中的分支以及在循环结构中的跳出循环或者进入循环。
6.2.2 If…… Then…… Else ……语句
If …… Then …… Else……语句用于有两个选择分支的语句中,当If后面的条件为真(True)时,执行Then……后面的语句,否则执行Else后面的语句。
例如:输入一个数,判断这个数是否大于100,大于100则输出“大于100”,不大于100则输出“小于等于100”。
Sub N()
Dim A As Integer
A=InputBox(“输入”)
If A>100 Then MsgBox(“大于100”) Else MsgBox(“小于等于100”)
End Sub
6.2.3 If…… Then 结构
If……Then结构可以用于多个选择条件,选择条件结束后,需要写明 End If。这个结构的格式如下:
If 表达式1 Then
语句1
ElseIf 表达式2 Then
语句 2
……
Else
语句n
End If
这种结构的运行顺序,先判断表达式1,表达式1成立就进行Then后面的表达式,否则再判断表达式2……所有表达式均不成立,执行Else后的语句。
例如,根据学生成绩给学生写评语,90分以上为“优秀”,80分以上为“良好”,70分以上为“中等“,60分以上为”及格“,60分以下为”不及格“。
Sub M()
Dim Score As Integer, V As String
Score=InputBox(“输入”)
If Score>=90 then
V=”优秀”
ElseIf Score>=80 Then
V=”良好”
ElseIf Score>=70 Then
V=”中等”
ElseIf Score>=60 Then
V=”及格”
Else
V=”不及格”
End If
MsgBox(V)
End Sub
6.2.4 Select Case 结构
Select Case结构同样可以用于对多个分支的选择,这种结构的语句如下:
Select Case 表达式
Case 值1
语句1
Case 值2
语句 2
……
Case Else
语句 n
End Select
Select Case的作用是,根据表达式的数值,选择Case中相应数值后面的语句。当表达式不等于所列的所有的数值,执行Case Else的语句。
例如,某商场促销活动,根据顾客购买的某商品个数定单价,当购买1件商品时没有优惠政策,购买2~5件商品时,按照原价9折优惠,购买6~10件商品按照8.5折优惠,10件以上一律8折。假设某衬衫原价为30.5元/件,输入购买件数,输出总金额。
Sub S()
Dim P As Double, Account As Double, N As Double, T as Double
P=30.5
N=InputBox(“输入件数”)
Select Case N
Case 1
Account=1
Case 2,3,4,5
Account=0.9
Case 6,7,8,9,10
Account=0.85
Case Else
Account=0.8
End Select
T=P*N*Account
MsgBox(T)
End Sub
6.2.5 分支结构的嵌套
分支结构的嵌套确实是一件比较令人头疼的工作,各种一层套一层的If……Else语句时常让人摸不着头脑。遇到这种分支嵌套的时候,通常的解决办法是尽量简化嵌套的结构,可以利用其他的逻辑判断方法来完成多层的判断。
例如,判断一个年分是否为闰年。闰年的定义是:如果年份不能被100整除,则判断年份是否能被4整除,被4整数就是闰年,否则不是闰年;如果年份能够被100整除,则年分需要被400整数才是闰年。
如果按照这样的逻辑判断,那么写成两层判断结构就在所难免了。
Sub LeapYear()
Dim Leap As Boolean
Dim Y as Integer
Y=InputBox(“输入年份”)
If Y Mod 100 <>0 Then
If Y Mod 4=0 Then
Leap=True
Else
Leap=False
End If
Else
If Y Mod 400=0 Then
Leap=True
Else
Leap=False
End If
End If
If Leap=True Then
MsgBox(“闰年”)
Else
MsgBox(“不是闰年”)
End If
End Sub
当然计算机可以明白这么多的If…… End If是怎么回事,也知道该如何去做。但是,作为编写计算机程序的人来说,这么多的嵌套看上去就非常麻烦,而且很容易自己把自己给弄晕了。我们可以把上面的这么多的判断简化一下,关于闰年的判断方法,可以表述为:能够被4整除且不能被100整除的年份或者能够被400整除的年份为闰年,否则不是闰年。这样我们就避免了分支的嵌套。
Sub LeapYear()
Dim Leap As Boolean
Dim Y As Integer
Y=InputBox(“输入年份”)
If (Y Mod 4=0 And Y Mod 100<>0) Or Y Mod 400=0 Then
Leap=True
Else
Leap=False
End If
If Leap=True Then
MsgBox(“闰年”)
Else
MsgBox(“不是闰年“)
End If
End Sub
还有另一种方法避免分支结构的嵌套,即利用 If……Goto的方法,一般地,可以在程序段中加入一个行标,行标可以用英文字母加上数字构成,并在行标后加上冒号。但是,行标只是起到标识和引导的作用,并不会阻止程序继续向下执行,所以,如果需要退出则额外需要Exit Sub这样的命令。
例如,判断单元格A1中的数字大小和背景颜色。规则如下:数字小于10的时候,如果背景颜色为红色,则输出”安全运行“,否则输出”警告“;数字大于10的时候,背景色为红色则输出”危险“,否则输出”停止运行“。
Sub NC()
With Range(“A1”)
If .value<10 Then Goto L1 Else Goto L2
L1:
If .Interior.Color=vbRed Then MsgBox(“安全运行”) Else MsgBox(”警告“)
Exit Sub
L2:
If .Interior.Color=vbRed Then MsgBox(“危险”) Else MsgBox(”停止运行“)
End With
End Sub
这里 Goto的作用即让计算机直接跳到与Goto后面所列的行标相同的地方执行。
第三节 循环结构
6.3.1 循环结构和死循环
在计算机编程中,循环结构是使用范围最为广泛,使用频率作为频繁的结构。循环结构就是让计算机循环往复的做一件事情,直到达到某种目标。而往往由于循环结构设计的不好,导致计算机陷入某种循环,永远也循环不出来,这种情况叫做”死循环”。举一个日常生活中的例子。领导要求业务员打电话给某人,业务员拨通电话后,无人接听。这种情况很常见,我们一般会等一会再打,实在打不通还可以向上报告领导。但是,如果这件事情交给计算机做,由于计算机忠实的执行你所有的命令,所以它就会不知疲倦地打电话,一直没人接听就一直打,一直打到对方电话没电或者自己死机。这种一直打不通电话一直打的情况就是“死循环”。
由此可见,计算机一旦陷入“死循环”,往往文件损坏或者系统损伤等严重的后果。所以,在设计循环之前,需要认真阅读本章节的内容。
6.3.2 If……Goto循环
利用If……Goto语句可以做一个循环。因为在上一节已经说到,Goto语句的作用是让计算机跳转到某一个标识行,如果我们把标识行放在Goto语句的前面,那么计算机跳转到前面,然后再次顺序执行下来,又会跳转回去。为了能够使得计算机从这样循环往复的跳转中“解脱”出来,我们采用If语句进行判断,符合条件则跳转回去,否则不跳转,直接执行下面的代码。
例如,要求用户输入手机号码,首先一条,手机号码都是11位的,如果直接采用InputBox,用户可以任意输入若干个数字,所以我们这里可以采用一个循环结构,让用户输入11个数字,如果输入不是11个,则让用户重新输入。
Sub Tel()
Dim T As String
L1:
T=InputBox(“请出入号码,11位”)
If Len(T)<>11 Then Goto L1
MsgBox(“Tel:” & T)
End Sub
这个程序中,让用户输入一个电话号码,判断电话号码是否为11位,即If Len(T)<>11,如果不是11位,则跳转(Goto)到L1的位置,L1位于If…… Goto语句前面。所以完成了用户只能输入11位数才可以正常通过的设计。但是,这里有一个小问题,即用户还是可以输入11个字母,或者11个数字加字母,只要是11个“字符”就可以。那么,如何让用户只能输入11位数字呢?假设用户输入的都是手机号码,我们先不去验证这些手机号码是否正确,那么就需要在If语句后面添加一些条件。
Sub Tel()
Dim T As String
L1:
T=InputBox(“请输入号码,11位”)
If Len(T)<>11 Or CStr(Val(T))<>T Then Goto L1
MsgBox(“Tel:”& T)
End Sub
这个程序可以让用户输入开头不是0的连续11位数字。
6.3.3 For……Next循环
不得不说,If ……Goto循环是一个非常糟糕的循环,原因就在于Goto语句可以让计算机不加检查地跳转到程序的任何地方,这样往往造成思路混乱,产生程序编写者不希望的一些结果。
在循环次数已知的情况下,For……Next循环是一个不错的选择,在For……Next循环中,可以用Exit For来提前结束循环。
例如,计算1+2+4+8+……256。我们可以设计这样一个循环,让i分别从0一直循环到8,然后声明一个变量S,每一次循环就让S加上2的i次方(分别等于0,1,2,……256)
Sub TEST()
Dim S As Integer, i As Integer
For i =0 to 8
S=S+2^i
Next i
MsgBox(S)
End Sub
这一段程序需要解释的地方是这个For……Next里面的东西。For 这一行,首先i=0,表示变量i的初始值为0,to 8,表示循环结束的标准是i不大于8,For这一行后还可以加上Step表示每循环一次i的数值增加多少,默认Step为1,使用默认Step时可以省略这个Step。中间的部分,S=S+2^i,即每循环一次的时候需要执行的任务,这里是每循环一次,就给S加上2的i次方这个数。Next i 表示一个循环结束,i的数值按照Step进行增加,进行下一个循环。
可以利用For…… Next构建双重循环,双重循环即在一个For循环内部还有另一个For循环,双重循环运行的顺序是,先进性内部的循环,内部循环结束后,在进行外部循环。
例如,构建一个9*9乘法表。
Sub Multi()
Dim i As Integer, j As Integer, k As Integer, StrL As String
For i=1 to 9
For j=1 to i
k=i*j
StrL=StrL &j & “*” & i & “=” & k & Chr(9)
Next j
StrL=StrL & Chr(13)
Next i
MsgBox(StrL)
End Sub
这一段程序中,首先看到大循环i=1 to 9,然后是内部循环 j=1 to i,即对于每一个i,j的数值都从1循环到i。所以执行顺序是,首先i=1,执行循环j=1 to 1,然后 i=i+1=2,执行循环j=1 to 2……
Chr(9)和Chr(13)分别是制表符(Tab键)和回车符(Enter键),用于生成水平制表以及回车(换行)。
6.3.4 提前退出循环
有时候我们需要提前退出循环,例如,求A列前20个数的和,但当遇到空格的时候,提前结束求和,即空格下方的数据不参与求和。这时,我们可以用Exit For来提前结束循环。
Sub SIS()
Dim S As Double, i As Integer, Rng As Range
Set Rng=Range(“A:A”)
For i=1 to 20
If Rng(i)=”” Then Exit For
S=S+Rng(i)
Next i
MsgBox(S)
End Sub
6.3.5 For Each ……Next循环
For Each……Next循环用于循环个数不确定的情况,是对于一个集合中的所有元素进行循环。例如,在Range对象中,当我们不知道Range对象的大小时,可以用For Each……Next循环一一遍历Range对象中的单元格。例如,对于用户任意选定的区域,求选定区域中所有数字的绝对值之和。
Sub ASB()
Dim Rng As Range, R,S As Double
Set Rng=Application.InputBox(“选择区域", , , , , , , 8)
For Each R In Rng
S=S+Abs(R.Value)
Next R
MsgBox(S)
End Sub
6.3.6 While…… Wend循环
当知道循环结束的条件,而循环次数不确定时,可以用当前循环或者直到循环,While……Wend循环即为当前循环。While……Wend循环执行的顺序是,判断While后面的条件是否成立,如果成立则进入循环,否则跳出循环。
例如,输入一个数,现在需要将这个数转化为2进制形式表达出来。
在第3章求2进制的讲解中,我们知道,求一个数的2进制表达形式,就是不断地用这个数除以2,余数作为数的每一位,直到这个数等于0。
Sub Bin()
Dim N As Integer, Str As String
N=InputBox(“输入数字”)
While N<>0
Str=N Mod 2 & Str
N=N\\2
Wend
MsgBox(Str)
End Sub
6.3.7 Do……Loop循环
有时候,确实需要构建一个“死循环”,即当循环的次数以及循环条件都不是非常确定时,我们采用这种循环。Do……Loop可以用于构建这种“死循环”构建这种死循环的时候需要注意,循环内部一定要提供跳出循环的条件语句,即Exit Do。
例如,做一个Excel工作簿保护程序,要求用户输入密码,当输入错误达到3次时,关闭Excel工作簿。(假设密码为123456)
Sub Pro()
Dim Str As String, T As Integer
T=1
Do
Str=InputBox(“请输入密码”)
If Str=”123456” Then
Exit Do
ElseIf T<3 Then
MsgBox(“密码错误!”)
T=T+1
Else
MsgBox(“输错3次了!”)
ThisWorkBook.Close
End If
Loop
End Sub
6.3.8 Do While …… Loop循环
Do While…… Loop循环也是当前循环,是当循环条件不达到时退出循环,While后可以添加循环条件。与Do……Loop循环不同的是,这里是先对循环条件进性判断,然后决定是否进入循环。
就上面让用户输入3次密码的程序,改写成Do While……Loop循环。
Sub Pro()
Dim Str As String, T As Integer
T=1
Do While T<=3
Str=InputBox(“输入密码”)
If Str=”123456” Then Exit Sub
MsgBox(“密码错误!”)
T=T+1
Loop
MsgBox(“输错3次了!”)
ThisWorkBook.Close
End Sub
6.3.9 Do Until……Loop循环
Do Until……Loop循环为直到型循环,即判断条件是否符合,符合条件退出循环,否则执行循环。
例如,让用户猜一个100以内的整数,让用户输入的数字大于目标数,输出“大了”,小于目标数,输出“小了”,直到用户出入正确。
Sub Game()
Dim T As Integer, A As Integer
Randomize
T=Fix(Rnd()*100)
A=-1
Do Until T=A
A=InputBox(“输入数字”)
If A>T Then
MsgBox(“大了”)
ElseIf A<T Then
MsgBox(“小了”)
End If
Loop
MsgBox(“对了!”)
End Sub
这里,Randomize的作用是,让每一次运行产生的随机数均不相同。首先给A赋值为-1的作用是随机数Rnd()可能产生的数为从0到1,乘以100以后,等于从0到100。若随机数刚好产生0,若A一开始没有赋值,其为0,用户不用输入则程序自动跳过循环体。
6.3.10 Do…… Loop While循环
Do……Loop While循环也是当前循环,与Do While ……Loop不同的是,Do……Loop While循环先进性循环,然后判断条件是否满足,满足条件则返回循环体。
例如,让计算机随机的写一些20以内的加法算式,当写到大于20的时候就结束。
Sub Cal()
Dim A As Integer, B As Integer, C As Integer
Randomize
Do
A=Fix(Rnd()*20)
B=Fix(Rnd()*20)
C=A+B
MsgBox(A & “+” & B & “=” & C)
Loop While C<=20
End Sub
运行一下,可以看到,这个程序每一次运行结果都不一样,每一次运行次数也不一样。
6.3.11 Do……Loop Until循环
Do……Loop Until循环为直到型循环,与Do……Loop While循环一样,这个循环也是先进性循环再判断条件。
例如上述程序,改成成Do……Loop Until结构:
Sub Cal()
Dim A As Integer, B As Integer, C As Integer
Randomize
Do
A=Fix(Rnd()*20)
B=Fix(Rnd()*20)
C=A+B
MsgBox(A & “+” & B & “=” & C)
Loop Until C>20
End Sub
第四节 VBA中的过程
6.4.1 Sub过程
早在第二章的时候,我们就已经认识到了VBA中Sub过程和Function过程两个过程。这里我们进一步对这两个过程进行解释。
(1) Sub过程
Sub是Subroutine(子程序)的简写,是由某个比较大的任务分解而来的小任务。在日常生活中,我们也可以遇到许多这种小任务,例如把“烧水泡茶”这项任务拆分,可以分为“接水”、“点火”、“准备茶具”、“抓茶叶”、“泡茶”等小任务。在VBA中,我们把一项任务看成一个“工程”,那么这项工程是由这些子程序,即Sub的过程组成。一项工程可以只有一个子程序,也可以有若干个子程序。
Sub过程只是简单地执行某项任务,并不能给我们某种“反馈”,即其并不可以返回一个具体的数值。要想得到某种“反馈”必须在Sub过程中运用某种方法把信息曾现出来。例如,显示一个对话框,让用户选择“是”或者“否”从而决定是否关闭Excel工作簿。
Sub Y()
Dim K
K=MsgBox(“是否关闭工作簿?”,vbYesNo)
If K=vbYes Then
ThisWorkBook.Close
Else
Msgbox(“没有选择关闭”)
End If
End Sub
(2) Sub 过程调用Sub过程
在Sub过程中,可以调用其他的Sub过程,这就好比我们平时可以一边接电话一遍记录电话内容一样。在一个Sub过程中,调用另一个Sub的方法是使用Call关键字,然后输入另一个Sub过程的名字。例如,我们先在A1单元格里面写入数字“2019”,然后再让这个单元格里面的数字加上1,显示出来。可以定义两个Sub,一个是向A1中写2019这个数,另一个,是让A1中的数加上1显示出来。然后用后面一个过程调用前面一个过程,这样你不需要调用单独调用前面一个过程,前面一个过程就已经自动执行了。
Sub W1()
Range(“A1”)=2019
End Sub
Sub W2()
Dim a As Integer
Call W1
a=Range(“A1”)+1
MsgBox(a)
End Sub
在点击“运行”的时候,直接选择运行W2过程,而不运行W1过程。可以看到,运行W2过程的同时,W1过程也得到运行。即同时在A1单元格输入数字2019以及显示一个对话框。
(3)有参数的Sub过程
参数就是在进行某项任务时,与任务有关的,但可以改变的一些数值,例如,“跑步100米”这项任务中,“跑步”是要做的任务,100米是这项任务的“参数”可以把100米改成1000米,“跑步”的任务任然没有发生变化。在Sub过程中,可以引入一些“参数”,这些参数提供给Sub过程某种“目标”。例如,用Sub过程对两个数进行“交换”需要交换的两个数就是“参数”。我们一起来看一下这个“交换”的过程。
首先,假设现在有两个数a和b,让a=1,b=2。任务是:交换以后,a的数值变成2,b的数值变成1. 读者可以先想一下怎么做?
也许你会觉得简单,直接让a=b,然后b=a就好了。仔细想一想,就会发现这样不对!因为,当a=b的时候,计算机把b的数值赋值给a(记住,=是赋值符号!),现在a和b就都等于2了!然后再让b=a,b的数等于没有变!这样看来计算机编程还真是需要一点逻辑的!
这里,我们需要运用一个临时的变量T来保存一下a的数值,然后把b的数值赋值给a,再回过头来把T的数值赋值给b。就像这样T=a a=b b=T。现在运用计算机的思维,第一步T=a,T的数等于1,第二步,a=b,a的数等于2,第三步,b=T,b的只等于1.这样就完成了交换,于是,这个Sub过程这样写。
Sub Swar( a As Integer, b As Integer)
Dim T as Integer
T=a
a=b
b=T
MsgBox ("a=" & a & ",b=" & b)
End Sub
Sub Test()
Dim a As Integer, b As Integer
a=1
b=2
Call Swar(a,b)
End Sub
这里,我们看到,调用一个有参数的Sub过程方法是,Call关键字加上名称再加上需要的参数。就像Call Swar(a,b)这样,这一句意思是,让Test这个过程中的a作为参数传递给Swar这个过程中的a,Test过程中的b作为参数传递给Swar过程中的b。
(4) 参数地址传递和值传递
如果我们在上述程序中添加一个MsgBox显示一下Test中的a和b的数值:
Sub Swar( a As Integer, b As Integer)
Dim T as Integer
T=a
a=b
b=T
MsgBox ("a=" & a & ",b=" & b)
End Sub
Sub Test()
Dim a As Integer, b As Integer
a=1
b=2
Call Swar(a,b)
MsgBox ("a=" & a & ",b=" & b)
End Sub
运行一下,可以看到,在Test过程中,a和b的值也发生了交换。这是因为这个Swar过程中的两个参数是传地址引用的。如果我们把这段程序简单修改一下:
Sub Swar( ByVal a As Integer,ByVal b As Integer)
Dim T as Integer
T=a
a=b
b=T
MsgBox ("a=" & a & ",b=" & b)
End Sub
Sub Test()
Dim a As Integer, b As Integer
a=1
b=2
Call Swar(a,b)
MsgBox ("a=" & a & ",b=" & b)
End Sub
只是简单地加上了两个词,ByVal,再次运行,读者会发现,两次的MsgBox中,只有第一次的时候a和b的数值发生了交换,第二次并没有发生交换!
细心的读者可能会问这是为啥。简单地来说,第一段Swar中参数a和b为传地址,第二段a和b为传值。何为传值何为传地址?
简单介绍一下,在计算机中,对于某一个数的存储,其用到一块内存,这一块内存在计算机的内存中具有一个编号,这个编号就是这块内存的“地址”。现在不妨把计算机内存当作“旅社”地址就是这个旅社中的房间号码。
举个例子,上面这个变量a,在计算机会开辟有一块内存空间,这一块内存空间的名字叫做a,a的数值等于1,也就是内存中所储存的东西为数字1,而这一块内存空间在计算机内存中的编号是不会发生变化的,假定为H10000001。
现在如果运用传地址的工作方式,则计算机在执行Swar这个过程的时候,看到a,就直接找到内存的H10000001位置,然后对其进行操作。而如果采用传值的工作方式,计算机在执行Swar这个过程的时候,并不是找到内存的H10000001位置直接进行操作,而是“复制”这个地址里面的数值,也就是1到另一块内存里面,然后再对另一块内存进行操作。
在上述的例子中,第一段程序里面没有ByVal关键字,系统将自动按照“传地址“方式进行工作,(默认关键字为ByRef,可以不写),而第二段程序中加入了关键字ByVal,系统将按照”传值“方式进行工作。(图6.2、6.3)
图6.2 传地址(红色为函数过程,黑色为传参数过程)
图6.3 传值
6.4.2 Function过程
与Sub过程不同的是,Function过程可以返回一个具体的“数值”,Function这个单词即英语中的“函数”,VBA的Function过程也就是数学里面的“函数”,像Excel工作表中的“函数”一样,Function过程也可以返回一个具体的数值。接下来我们看几个例子说明这个过程的用法。
(1)定义一个Function过程,求两个数中较大的一个数。
Function Max( A As Integer, B As Integer) As Integer
If A>B Then
Max=A
Else
Max=B
End If
End Function
这里我们可以看到已经熟悉的If条件语句,这个函数编写完之后,打开Excel工作簿,可以直接在任意一个单元格里面写上“=Max(”可以看到,我们自己写的这个Function函数可以像Excel内在函数一样使用。在Excel中使用自己定义和编写的函数与使用内置函数的过程是一样的)函数的返回值等于这个函数的名称,即这个函数名为Max,函数最后的数值等于在函数中的Max这个变量的数值(定义了Max为函数名的同时也就定义了Max这个变量了,没有必要在函数中再次定义Max变量)。
(2) 定义一个Function过程,求一个单元格区域中最大的数字
Function MaxM(Rng As Range) As Double
Dim R
MaxM=Rng.Cells(1,1)
For Each R In Rng
If R.Value>MaxM Then MaxM=R.Value
Next R
End Function
函数既然是一个“过程”那么其也可以运用例如For,For Each等循环或者分支结构。这里先对MaxM赋初值为单元格区域的第一个单元格的数字,然后一一比较MaxM与单元格中数值的大小,并把较大的数赋值给MaxM。
(3)定义一个Function过程,要求函数有一个可选参数,参数选择True时返回单元格区域的最大值,参数选择False时,返回最小值。
Function M(Rng As Range, Optional OP As Boolean=True) As Double
Dim R
M=Rng.Cells(1,1)
For Each R In Rng
If OP=True Then
If R.Value>M Then M=R.Value
Else
If R.Value<M Then M=R.Value
End If
Next R
End Function
这个函数在Excel中运用时,如果不加第二参数,则返回一个单元格区域的最大值,例如”=M(A1:A8)”将返回A1到A8区域中的最大值。如果加上第二参数,并且第二参数为False则返回该区域的最小值,例如“=M(A1:A8,False)”。
(4)定义一个函数,现在要求一次性返回两个数,第一个数表示单元格区域的大小(共有多少个单元格)第二个数表示这些单元格中的数字之和。
Function MySum(Rng As Range)
Dim S As Double, N As Integer,R
Dim A(1) As Double
For Each R In Rng
N=N+1
S=S+R.Value
Next R
A(0)=N
A(1)=S
MySum=A
End Function
在使用这种一次返回一个数组的函数时,需要根据返回数组的大小选定单元格(本例子中为选定两个单元格)然后输入“=MySum(”,再使用Shift+Ctrl+Enter方式就可以完成对这个公式的运用了。
(5)ParamArray,ParamArray可以让函数有一个不确定参数个数的参数形式。例如,我们随机选择许多个单元格区域,并且求所有区域中的所有单元格数字之和。
Function SumAny(ParamArray Group())
Dim S As Double,i,j
For Each i In Group
For Each j In i
S=S+j
Next j
Next i
SumAny=S
End Function
这个函数中,使用两个循环变量对单元格进行遍历,第一个变量i用于遍历所有的选定单元格区域,第二个变量j用于遍历每一个单元格区域中的每一个单元格。
(6)函数的调用
函数的调用和Sub的调用相似,函数的调用不需要用Call关键字,可以直接调用。例如,我们可以写一个拆分一个合数为两个素数的函数,这个函数又调用到判断一个数是否为素数的函数。
Function IsPrime(N As Integer) As Boolean
Dim i As Integer
IsPrime=True
For i =2 to N-1
If N Mod i =0 then IsPrime=False: Exit For
Next i
End Function
Function Gid(N As Integer)
Dim A(1) As Integer, i As Integer
For i =2 to N-2
If IsPrime(i) And IsPrime(N-i) Then A(0)=I : A(1)=N-I : Exit For
Next i
Gid=A
End Function
这里,Gid直接调用函数IsPrime。
(7)函数的递归
记得在大学信息技术考试的时候,其中一道名词解释题目就是“递归”。我的答案是这样写的:
递归:递归。
也许有人要问,你这不是没有解释么?能获得分数么?结果是,我这个名称解释被老师评为“最为恰当的名词解释”。即递归就是“递归”本身。相信读者看到这里会觉得一头雾水,那么我们就给出一个比较通俗一点的解释。
用通俗一点的解释来说,递归就是一个函数自己解释自己,自己调用自己的过程。在生活中可以举一个例子,小明有一个姐姐和一个哥哥,已知道小明去年12岁,小明的姐姐比小明大2岁,小明的哥哥比姐姐大3岁,问小明哥哥的年龄。
这个例子中,我们可以进行如下推导:
第一,要求哥哥的年龄,需要知道姐姐的年龄,哥哥的年龄=姐姐的年龄+3
第二,求姐姐的年龄,需要知道小明的年龄,姐姐的年龄=小明的年龄+2
第三,求小明的年龄,小明去年12岁,于是小明的年龄=12+1
再次推到哥哥的年龄,哥哥的年龄=姐姐的年龄+3=小明的年龄+2+3=12+1+2+3=18.
这种根据一个基础条件不断向后推导的过程就叫做“递归”,在计算机中,可以自动完成这种推导的过程。
举个例子,斐波那契数列中,第一个数和第二个数都是1,以后每一个数等于前面两个数的和。于是,我们可以得到这样的数列: 1,1,2,3,5,8,13,21,34……如果用VBA求这个数列的第N个数。则可以写成这样的函数
Function Fib(N As Integer) As Integer
Dim F As Integer
If N=1 Or N=2 Then
F=1
Else
F=Fib(N-1)+Fib(N-2)
End If
Fib=F
End Function
这个函数Fib中,运用了函数的递归调用。我们来看一下这个函数,如果输入Fib(4),VBA会干那些事情。
第一步,计算Fib(4)。判断N的数值是否等于1或者2,现在N=4,不等于1,也不等于2,所以执行F=Fib(N-1)+Fib(N-2),即F=Fib(3)+Fib(2)
第二步,计算Fib(3)。现在N=3,执行F=Fib(2)+Fib(1)
第三步,计算Fib(2), 等于1,于是现在变成F=Fib(2)+Fib(1)+1
第四步,计算Fib(2), 等于1,现在变成F=1+Fib(1)+1
第五步,计算Fib(1), 等于1,现在变成F=1+1+1=3
第六步,执行Fib=F,所以最后函数Fib的数值等于3.
(8)利用函数完成高等数学中“无穷大”的思想
计算上面这个斐波那契数列后面一项与前面一项的比值,看一下当N趋近于无穷大的时候这个比值为多少。
计算机并不会知道无穷大这个要怎么计算,我们唯一的方法,是让计算机计算很多次,每两次的数值进行比较,如果两次数字相差小于一个非常小的数,就认为我们执行到了“无穷大”次。
在上述Function的基础上再添加一段代码
Function P() As Double
Dim N As Integer
N=1
While Abs(Fib(N+1)/Fib(N)-Fib(N+2)/Fib(N+1))>1E-5
N=N+1
Wend
P=Fib(N+1)/Fib(N)
End Function
与上述的函数不同的是,这个函数并没有“参数”,然而却不妨碍这个函数的运行,在Excel的单元格里面输入“=P()”,按下确定。
可以看到,这个比值为1.618. 说明斐波那契数列后面一项与前面一项的比值,随着项数的增加,无限趋近这个数字。可能对于某些读者来说,1.618是一个非常熟悉的数字,没错这个数字就是黄金比例0.618的倒数,也就是说,斐波那契数列的前后两项比值无限趋近于黄金比例。
利用相同的原理,我们也可以自定义一个计算自然数对数底e的函数。
Function SumE(N As Integer) As Double
Dim I As Integer
For I=1 to N
SumE=SumE+1/ Application.WorksheetFunction.Fact(N)
Next I
End Function
Function E() As Double
Dim N As Integer
N=0
While Abs(SumE(N+1)-SumE(N))>1E-5
N=N+1
Wend
E=SumE(N+1)
End Function
但是这个函数并不像之前那个P()表现的这么好,数值等于3.0了都。这是因为我们这个算法不够精确导致的。因为计算机中对于一个小数的存储和运算时,总会丢失小数后若干位。所以这样的算法并不能得到希望的解。现在我们把这个函数改写如下:
Function EN(N As Long) As Double
EN = (1 + 1 / N) ^ N
End Function
Function E()
Dim N As Long
N = 1
While Abs(EN(N + 1) - EN(N)) > 0.00001
N = N + 1
Wend
E = EN(N + 1)
End Function
再次运行,这一次的数值等于2.7146,比之前精确许多。读者可以自行想一下如何写一个计算圆周率的函数。