第五章 函数和过程
procedure过程,没有返回值;
function函数,必须指定返回值;
二者统称例程。
5.1例程的声明
procedure 过程名(形参列表);[指示字];[调用约定;]
function 函数名(形参列表):返回值类型;[指示字];[调用约定;]
形参列表
一般形式
var 参数1: 类型名1; var 参数2: 类型名2; … var 参数n: 类型名n
var是参数的传递方式,可用out或const代替;
同一例程中不允许有同名的参数;
列表中的每个形参项后以分号结尾,最后一个不能有分号;
返回值不能是任何形式的文件类型;
指示字为某些特定的关键字,用virtual、abstract等,在声明例程时可以不加任何指示字;
调用约定在后续章节提到;
var p1, p2:integer是可以的;
external声明
举例
procedure sample(var s:string); external 'SomeDLL.dll';
当需要从其他编译单元.dll文件中引用一个例程时,如上可以声明;
external是.dll的完整名称包括路径名和文件名,除非.dll和当前编写的程序位于一个文件夹可以不加路径;
procedure sample(var s:string); external;
当需要从其他编译单元.obj中引用时,如上可以声明;
声明前需要在程序中引用相应的.obj文件,格式为;
{$L BLOCK.OBJ}
forward
用于声明一个例程,它可以使例程在定义前就被使用;
举例
program forward_sample;
{$APPTYPE CONSOLE}
uses
SysUtils;
procedure fun1; forward;//此处的forward去掉无法通过编译
procedure fun2;
begin
fun1;
end;
procedure fun1;
begin
//
end;
begin
end.
5.2例程参数
形参和实参
procedure Sample(var S:string; var I:integer);
这里S和I就是形参,形参不能是静态数组及文件类型;
Sample('delphi', 101);
这里’delphi’和101就是实参;
参数传递方式:传址方式,传值方式,常量方式,输出方式;
传址方式
procedure var_sample(var I:integer);
begin
I := 2 * I;
Writeln(I);
end;
var
N:integer;
begin
N := 9;
var_sample(N);
Writeln(N);
Readln;
end.
var声明的参数采用传址方式传递,这种参数将变量的地址传给例程,如果形参的值改变,实参的值也会改变;
显示I和N都是18,可以证实确实改变了实参的值;
调用例程时传给var参数的一定是可以被赋值的变量或者表达式,决不能是常量,除非:
-
当一个对象类型的常量传入例程时,对象的属性可以被改变;
-
如果开启了
{$J+}
开关,常量与变量无甚区别,此时也可以将常量传入,out也是这样;
传值方式
procedure sample(i:integer);
没有任何关键字的参数采用传值方式传递,这种方式传递的参数值也会发生变化但是影响不到原来的变量;
实际上系统在参数传递前将参数复制了一份,然后将复制的新变量传给例程,例程运行完毕后,新变量被销毁;
常量方式
procedure const_sample(const I:integer);
使用const声明的参数任何时候都不会被改变,强行改变会编译错误;
除非是对象引用或者是指针,依然可以改变对象的属性值或字段值,也可以改变指针所指向的变量的变量值;
举例
type
pi = ^integer;
procedure p_sample(const I:pi);
begin
I^ := 2 * I^;
Writeln(I^);
end;
var
N:integer;
begin
N := 9;
p_sample(@N);
Writeln(N);
Readln;
end.
注:^integer并非合法标识符所以先声明新类型;
I不可以指向其他的变量,但是I指向的变量的值可以被改变。
输出方式
function sample1(S:string):integer;
begin
result := length(s);
end;
procedure sample2(S:string; out Leng:integer);
begin
leng := length(s);
end;
var
I, N:integer;
S:string;
begin
S := 'delphi';
I := sample1(S);
sample2(S, N);
end;
注:I和N的值都是6;
与普通函数返回值不同的是,out可以同时返回多个值;
其实var和out在使用上完全一样,区别是out参数传入例程前,系统会自动清空变量原来的值,var没有这种处理;
举例
procedure sample(out S:string);
begin
//do nothing
end;
var
str:string;
begin
str := 'delphi';
Writeln(str);//show delphi
sample(str);
Writeln(str);//show nothing
Readln;
end.
默认参数
function myfun(var I;integer = 3; var S:string = 'Delphi'):string;
如果调用此例程时没有指定参数S的值,则编译器会自动将字符串’Delphi’赋给S;
举例
myfun();
myfun(3);
myfun(3, 'Delphi');
以上结果均相同;
然而只有直接常量值可以指定给参数作为默认值,有些类型却根本没有常量值,所以这些类型就没有默认值;
这些类型包括:记录、变体、文件、静态数组、对象类型
以下类型的参数仅能使用nil作为默认值:
动态数组、例程类型、类、类引用、接口;
关于默认参数的规定:
1.当某一个参数被指定了默认值,此参数后的全部参数都要有默认值;
同理,在调用的时候,如果某个参数使用了默认值,此后的所有指定了默认值的参数均使用默认值;
2.当某个参数项有多个参数名时,不可以指定默认值;
举例
procedure sample(var a, b:integer);
3.当某个例程类型中指定了默认参数时,这些默认参数将掩盖具体函数中的默认值,之后在例程类型章节会详述;
如果一个例程有默认参数,则声明这个例程的时候就要指定这些默认值,在定义时可以省略不写,如果不省略应当与声明中的形式完全一样;
重载例程时,默认参数也很容易造成歧义,之后再重载例程章节详述。
特殊类型的参数:无类型参数,短字符串,数组;
无类型参数
function sample(var C):integer;
无类型参数的传递方式不可以是默认,可以是const,out,var;
调用例程时不可以将整型直接常量或值为整型的无类型符号常量赋予无类型参数,但是整型的类型常量可以;
举例
procedure fun(const c);
begin
//do nothing
end;
const
d = 987;//整型的无类型常量
c:integer = 987;//整型类型常量
begin
fun('897'); //ok
fun(c); //ok
fun(d); //worng ,dont do this
fun(876); //wrong,dont do this
end.
在例程运行期间,无类型参数与其他任何类型都不兼容,使用前必须进行类型转换,不过编译器并不确保这种转换一定有效;
举例
function Equal(var Source, Dest; Size: Integer): Boolean;
type
TBytes = array[0..MaxInt - 1] of Byte;
var
N : Integer;
begin
N := 0;
while (N < Size) and (TBytes(Dest)[N] = TBytes(Source)[N]) do
Inc(N);
Equal := N = Size;
end;
短字符串参数
声明方式:直接声明为shortstring类型;或者利用string后限定长度从而定义特定长度的字符串string[9]
;
但是在声明例程的参数时不能使用第二种方法,也就是说;
procedure Check1(var S: string[20]);
是错误的,不能通过编译;
取而代之,可以这样
type
Mystring = string[20];
procedure Check2(var S:Mystring);
注:
还可以使用openstring类型解决,此类型接受任何长度的短字符串,
如果同时开启{$H+}
和{$P+}
,则例程中string类型参数和openstring类型参数等价;
但是应当尽量使用长字符串AnsiString或者UnicodeString等;
实际上短字符串参数具有这种限定是因为短字符串是一个字符数组,就相当于
procedure Check1(var S: array[0..20] of ansiChar);
而根据后面一句所讲,不可以指定索引值,所以不能通过编译;
数组参数规则:声明一个数组类型的参数时不可以指定数组的索引值;
因此我们可以
type
myarray = array[0..9] of = Integer;
procedure sample(var a:myarray);
或者利用开放数组
procedure sample(var a: array of Integer);
由于开放数组参数的声明方式与动态数组一致,当我们需要声明动态数组变量时,要这样;
type
darray = array of Integer;
procedure sample(var a: darray);
开放数组和动态数组的区别是:
开放数组既可以容纳任意长度的静态数组,也可以容纳动态数组;
动态数组类型的参数只能接纳动态数组,无法接纳静态数组;
举例
program Sample;
{$APPTYPE CONSOLE}
uses SysUtils;
type
darray = array of Integer;//声明动态数组类型darray
procedure check(var s: array of integer);
begin
//do nothing
end;
var
s:array[0..9] of Integer;
begin
check(s);
end.
注:
如果把s的类型换成s:darray
也可以通过编译;
数组参数声明为什么不能指定索引呢?
var 参数: 类型名
类型名只能是标识符或者标识符的组合,显然string[20]
不是合法的标识符,[0..9]
也不是;
array of type
是标识符的组合,所以可以声明参数,同理,^type
也不可以声明一个指针类型的参数。
还有一类数组参数Variant Open Array Parameters
,变体开放数组参数;
举例
procedure M1(value:array of const);
注:array of const
实际上等效于array of Variant
;
举例
function MakeStr(const Args: array of const): string;
var
I: Integer;
begin
Result := '';
for I := 0 to High(Args) do
with Args[I] do
case VType of
vtInteger: Result := Result + IntToStr(VInteger);
vtBoolean: Result := Result + BoolToStr(VBoolean);
vtChar: Result := Result + VChar;
vtExtended: Result := Result + FloatToStr(VExtended^);
vtString: Result := Result + VString^;
vtPChar: Result := Result + VPChar;
vtObject: Result := Result + VObject.ClassName;
vtClass: Result := Result + VClass.ClassName;
vtAnsiString: Result := Result + string(VAnsiString);
vtUnicodeString: Result := Result + string(VUnicodeString);
vtCurrency: Result := Result + CurrToStr(VCurrency^);
vtVariant: Result := Result + string(VVariant^);
vtInt64: Result := Result + IntToStr(VInt64^);
end;
end;
注:这是一个case选择语句包在一个with语句里,仔细看清楚,end个数没错;
调用这个函数时
MakeStr(['test', 100, ' ', True, 3.14159, TForm]);
程序返回字符串
'test100 T3.14159TForm'
5.3例程的定义与使用
局部声明区域所声明的所有标识符包括变量名、常量名、类型名、套嵌例程名等只在本例程内有效;
局部声明区域所声明的所有标识符不能与例程名称、例程的参数名称相同,如果例程是一个函数,则标识符result也不能使用;
若本例程事先声明过,则定义此例程时,可以省略参数列表、返回值部分;
result是Delphi声明的一个预定义变量,其作为局部变量被隐含地声明于例程;
重要的是,result和return不同,程序不会因为result被赋值而中止;
函数名也可以作为返回值
举例
function sample: integer;
begin
sample := 98;
sample := 56;
end;
对于预定义的result而言,只有编译开关{$X}
处于{$X+}
状态时,才有效;
{$X+}
是系统的默认设置,只要不去手动改为{$X-}
即可;
相对于result而言,函数名不能作为变量进行类型转换等变量操作;
有时候函数名可能会被当成循环调用函数,因此建议都使用result。
函数的调用约定
Delphi提供五种调用约定
从左到右:pascal,register
从右到左:cdecl,stdcall,safecall
通常默认的register最为有效;
如果某个对象属性的访问权限是published,这个属性的读写方法必须使用register;
当调用某个使用C/C++编写的共享库.dll或.lib中的例程时,必须使用cdecl;
当调用其他的外部代码时,尽量使用stdcall或safecall,Windows API函数大多使用这两种方式。
例程的内嵌inline
调用一个例程并不会浪费多少时间,但是在一些需要频繁调用一些较为短小的例程时,浪费非常可观,因此需要使用inline;
编译器在编译inline例程时,会直接将某个例程的定义代码直接拷贝到调用它的位置
举例
function add(x, y: Integer):Integer; inline;
begin
result := x + y;
end;
var
s : Integer;
begin
s := add(2, 3);
Writeln(s);
end.
相当于
var
s:Integer;
begin
s := 2 + 3;
Writeln(s);
end.
内联例程能够提高程序的执行速度,但是增大了程序文件的体积,是以空间换时间;
并非所有例程都可以内嵌到目标代码,如
1.任何迟绑定方法包括virtual,dynamic,message不能内嵌;
2.含有汇编代码的例程不能内嵌;
3.类的构造函数和析构函数不能内嵌;
4.主程序块,单元的initialization和finalization部分中的代码不能内嵌;
5.单元中的内嵌例程先定义后使用,否则编译器无法得知该例程的实现;
6.含开放数组参数的例程不可内嵌;
7.包中的代码可以内嵌,however,inlining never occurs across packages boundaries;
8.循环引用的单元间不存在inline,但是其中的单元可以inline循环外的单元中的代码;
9.如果某个例程在interface部分声明但是其代码使用了定义于implementation部分的变量,则不可以inline;
10.如果某内嵌例程使用了其他单元中的代码,则其引用的单元必须全部在uses部分列出,否则不能内嵌;
11.如果while-do和repeat-util中使用的条件表达式含有例程,则此处的例程不会被内嵌,但在其他地方使用此例程可以内嵌;
举例
function add(x, y: Integer):Integer; inline;
begin
result := x + y;
end;
var
s, i: Integer;
begin
s := add(2, 3);//L1
while s < 100 do
if s > 78 then
s := add(s, 1);//L2
end.
通过运行显示,L1处正常运行得到5,L2处无显示,说明L2处的代码保持不变,不会被内嵌;
并且,编译器没有报错,说明编译器并非对每一种都会提出警告;
一般情况下,当某单元的interface部分发生变化时,所有引用此单元的其他单元都需要重新编译;
涉及内嵌例程时,稍有改变,只要一个内嵌例程的实现代码改变时,其所在的单元就被重新编译,所有引用此单元的其他单元也将重新编译。
Delphi提供了{$INLINE}
编译开关,用户可以手动控制编译器对内嵌例程的处理;
此开关的三个状态:
{$INLINE ON}
当例程后加上inline,会被内嵌
{$INLINE AUTO}
加inline会被内嵌,如果某例程的代码不大于32字节,即使未加inline也会被内嵌
{$INLINE OFF}
所有例程都不会被内嵌,即使加上inline
默认状态是{$INLINE ON}
5.4例程指针
在Delphi文档中称例程指针为过程类型,其所指的内容是一段代码构成的例程;
普通例程变量声明举例
function AddData(x:Integer):integer;
去掉AddData即可声明
var
F:function(x:Integer):Integer;
当满足下列全部条件时,编译器会认为两个例程变量等效:
1.参数的个数,类型,顺序完全一致;
2.返回类型完全一致(仅针对函数);
3.声明时使用的关键字相同(全是procedure或function)
举例
Type
MyData = Integer;
var
F1:function(x:Integer; y:Double):Integer;
F2:function(y:Double; x:Integer):Integer;
F3:procedure(x:Integer; y:Double);
F4:function(y:Double; x:Integer):string;
F5:function(x:Integer; y:Double):MyData;
F6:function(x:Integer; y:Double):Integer;
只有F1和F6是相等的;
例程变量的赋值举例
program Project1;
{$APPTYPE CONSOLE}
uses
SysUtils;
function AddData(X:Integer; Y:Real):Integer;
begin
result := 2 * X;
end;
var
F, Q:function(X:Integer; Y:Real):Integer;
begin
f := AddData;
q := f;
end.
赋值的两种方式:
将例程变量的值赋给另一个变量;
将一个定义好的例程名称赋给例程变量。
例程变量的使用举例
program Project1;
{$APPTYPE CONSOLE}
uses
SysUtils;
function LinkStr(s:String): String;
begin
result := '参数s的值为:' + s;
end;
var
F:function(s:String):String;
Str:String;
begin
F := LinkStr;
Str := F('this is Delphi2010');
Writeln(LinkStr('this is Delphi2010'));
Writeln(F('this is Delphi2010'));
Readln;
end.
例程变量的使用和普通函数没有任何区别;
对于过程变量,无论赋值与否,都应当用@@
才能获取地址值;
举例
program Project1;
{$APPTYPE CONSOLE}
uses
SysUtils;
procedure LinkStr;
begin
end;
var
F, Q:procedure;
v2:String;
begin
F := LinkStr;
Writeln(Integer(@@F));
Writeln(Integer(@F));
Writeln(Integer(@LinkStr));
Readln;
end.
@F
和@LinkStr
的值相同,都是例程LinkStr的地址,而@@F
是F本身的地址;
与大多数变量一样,例程变量的值可以是nil;
调用一个nil的例程变量会导致运行期错误,使用标准函数Assigned()
可以判断一个指针是否与某块内存绑定;
不是nil返回True,是nil返回False。
5.5匿名方法
函数引用、过程引用、方法引用合称例程引用;
例程引用类型变量的声明只有一种方式,先声明一个类型,再用这个类型声明一个变量;
举例
Type
TFun = reference to 本体;
var
RefFun:TFun;
本体就是一个省略名称的普通例程声明;
举例
Type
TFun = reference to function(x:Integer):Integer;
这跟例程指针很相似,其实除了内部细节有所不同,其他用法也极其相似;
举例
program RefSample_1;
{APPTYPE CONSOLE}
uses
SysUtils;
type
TFun = reference to function(x:Integer):Integer;
function fun(x:Integer):Integer;
begin
Writeln(x + 100);
end;
var
F:TFun;
begin
F := Fun;
F(12);
Readln;
end.
注:可以将一个普通例程赋予例程引用类型的变量;
如果在声明例程引用类型时指定了默认参数,则所有此类型的变量中将含有此默认参数;
举例
type
TF = procedure(s:String = 'default');
procedure echo1(s:String = 'delphi');
begin
Writeln(s);
end;
var
F:TF;
begin
F := ECHO1;
F;//显示default
Readln;
end.
其中F相当于
procedure F(s:String = 'delphi');
注意到ECHO1的值被覆盖,这正是Delphi默认参数的使用规定之一:
当某个例程类型中指定了默认参数时,这些默认参数将掩盖具体函数中的默认值。
如果TF中没有指定默认参数,无论F的值如何,调用时必须指定参数;
这里试验了将TF中默认参数去掉,直接F;编译错误,F('123')
;可以运行,echo1()
;也可以运行
未指定名称的例程称为匿名方法;
匿名方法是由一段代码构成的直接常量,可以将这种常量作为值赋给函数引用类型的变量;
举例
function myfun(x:Integer):Integer;
begin
//some code
end;
省略名称即
function (x:Integer):Integer;
begin
//some code
end;
这个匿名方法可以赋给一个函数引用类型的变量;
与普通例程不同,匿名方法不存在声明,其本身就是一个直接常量,只不过其值是可以运算的代码;
RefFun := function(x:Integer):Integer//这里不可以有分号
begin
//some code
end;
匿名方法的调用举例
program Sample;
{$APPTYPE CONSOLE}
uses
SysUtils;
type
TFun = reference to procedure(x:String);
var
F:TFun;
begin
F := procedure(x:String)
begin
Writeln(x);
end;
F('Delphi2010');
end.
将其赋予一个例程引用类型的变量,然后按照调用普通例程的方式调用这个变量即可;
匿名方法也可以直接作为一个例程的参数,由于匿名方法是一种直接常量,所以作为参数只能用const或value传递,不可以使用var或out;
举例
program RefSample;
{$APPTYPE CONSOLE}
uses
SysUtils;
type
TFun = reference to procedure(s:String);
procedure Fa(s:String; Fun:TFun);
begin
Fun(s);
end;
var
F:TFun;
begin
Fa('Delphi2010', procedure(s:String)
begin
Writeln(s);
end);//end后没有分号,注意
Readln;
end.
变量劫持举例
program RefSample;
{$APPTYPE CONSOLE}
uses
SysUtils;
type
TFun = reference to function(X:Integer):Integer;
function ReturnFun(y:Integer):TFun;
begin
Result := function(x:Integer):Integer
begin
Result := x + y;
end;
end;
var
fun:TFun;
begin
fun := ReturnFun(20);// 1
Writeln(fun(22));// show 42
Readln;
end.
当执行语句1时,程序会依次执行如下操作:
1.保存所有状态并离开当前区域进入函数RetunFun所在的区域以便运行此函数;
2.运行函数ReturnFun并获得一个TFun类型的值
3.携带ReturnFun的返回值回到原来的位置并将携带的值赋给变量fun
这里应当注意到,y没有在返回时被销毁,依然是20不变,它的生存期变得与匿名函数相等,
这种情况就是变量劫持:当某个局部变量被其所在例程中声明的匿名方法所使用,此变量的生存期将受匿名方法的控制而超出原来的生存期;
注:匿名方法只能俘获其父例程中定义的局部变量
编译代码时,如果编译器发现某个例程定义了一个匿名方法,其会自动产生一个与此例程相关联的数据结构,成为框架对象frame object,FO;
FO可以存放变量和匿名方法;
当编译器发现例程F1中的某个变量V被例程F2俘获,会把变量V记录在与F1相关联的FO_1上,
同时编译器为F2创建框架对象FO_2,并创建一个由FO_2指向FO_1的引用;
如果V同时被多个匿名方法俘获,系统会为每个匿名方法创建一个FO和一个指向FO_1的引用;
当出现下列情形时,引用会被销毁:
1.匿名方法超出的生存域期被销毁,此时其绑架的所有变量被同时销毁
2.例程引用类型的变量的值发生了改变,不再是原来的匿名方法
3.匿名方法被析构
当所有指向FO_1的引用都被销毁,系统会销毁变量V;
F1可以是普通例程或者匿名方法,F2一定是匿名方法;
F2一定是定义在F1中的。
5.6重载例程
重载举例
procedure Find(name:String);overload;
procedure Find(size:Integer);overload;
procedure Find(time:TDate);overload;
...
Find('d:\file\dest.txt');
系统会自动判断类型找到相应的例程,重载的重是多重而非重新;
重载是指同一个有效范围内某个例程名称同时对应多个不同的例程的定义;
同一个名称对应的多个例程必须全是过程或者全是函数;
声明一个重载例程只需在声明时加上限定符overload(定义例程时可以省略限定符);
例程的特征集:名称,参数个数,每个参数的类型;
例程的特征集不包括参数的传递方式,所以如下的例程重载无法通过编译:
procedure find(const c:String);overload;
procedure find(var c:String);overload;
系统寻找合适的find过程:
1.确定名称find,列出所有可用的find例程,这些例程的集合叫候选集;
2.确定参数数目和各自的类型;
3.寻找接受以上类型的find例程;
然而例程的默认参数和未指定数据类型的实参会影响到重载例程的选择;
举例
procedure fun(var s:string; var size:Integer = 1024);overload;//R1
procedure fun(var s:string);overload;//R2
procedure fun(var size:cardinal);overload;//R3
procedure fun(var size:integer);overload;//R4
如果fun('d:\file\dest.txt')
;
如何确定是R1还是R2?如果fun(1024)如何确定是R3还是R4?
Delphi并未提供默认参数的有效方法,故重载例程中不要定义默认参数;
对于某个实参,如果多个例程均符合,则调用参数数据类型值域最小的,这里integer比cardinal小,选R4;
如果实参是带小数点的实数,系统会转成extended类型,并寻找extended类型参数的例程,找不到则提示错误,除非有接受变体类型参数的例程;
如果实参是未指定具体数据类型的字符、字符串或字符指针(包括值为三者之一的符号常量和直接常量),系统会分别转为char,UnicodeString及pchar;
另外,变体类型的优先级低于所有简单类型,系统会先按照上述规则进行匹配,无法确定目标例程时,
系统会调用将参数看成是优先级最低的变体类型而调用接受变体参数的例程;
举例
procedure fun(s:byte);overload;
procedure fun(s:variant);overload;
这时
fun(256);
系统确定256是整数常量,整型按值域从小到大是:
shortint < byte < smallint < word < integer < cardinal < int64 < uint64
根据值域排除shortint和byte,在剩下的类型中按照值域从小到大进行筛选;
最终确定
procedure fun(s:variant);overload;