CnPack Tip#2 关于形参的分析
作者:LiuXiao,小冬,Bahamut, 考拉, SkyJacker
http://www.cnpack.org
CnPack IV QQ Group: 130970
2007-02-02
(转贴请注明作者、出处且保持完整)
一、形参的3种基本形式
function a(b: integer): integer;
function a(var b: integer): integer;
function a(const b: integer): integer;
b: 函数体内给b赋值后不会传出函数外
var b: 函数体内给b赋值后会传出函数外
const b: 函数体内不可以给b赋值
二、解释
一种是用值传递的方式,
也就是说,在参数传递的时候只把参数的值传入函数内部
另一种是地址传递的方式,在参数传递时传入参数的地址 function a(var b: Integer): Integer;
在调用A函数的时候 a(i); 其实参数是@i。
要清楚下面的解释过程,需要看一下 <<如何理解Move参数中的const Source和var Dest>>
地址:http://www.cnpack.org/showdetail.php?id=476?=zh-cn
(一种典型的上下文 context ?
)
在函数内部的Pascal实现中,Source是传入的地址再指了一次而得到的结果,
因此仍然代表传进来之前的Source。欲取得汇编中实现时传入来的地址,则需要用@Source。
也就是说,这个var封装并且隐藏了函数被调用时传入的取地址操作和函数体内部使用时指了一次的操作。
举个浅显的例子:
procedure(I: Integer);
var J: Integer;
begin
J := I;
end;
调用的时候,I的值被传进来,赋值给了J,这样理解没问题。
但如果是:
procedure(var I: Integer);
var J: Integer;
begin
J := I;
end;
那么,在底层实现中,也就是在汇编中,I的地址被传进来了,
然后函数内部指了一次,取到了它的值,赋值给了J。
但对外来讲,J := I;这句本身所完成的功能还是没有变化。
但如果在函数体里头写 I := 0,那么这俩函数的实现功能就有区别了。
不带var的,传入的是一个I的值,这个值存在一个临时的地方,可能是寄存器,也可能是堆栈区。
I := 0;就是把这个临时地方的值塞成0.
带var的,传入的是I的地址,这个地址值也存在一个临时地方。
但赋值的时候,这个地址指了一次,于是就指向传入前的I了,那么I:= 0就把传入前的变量的值给改变了。
三、测试实例
根据以上的介绍,上机测试了一下。学习乃学而时习之嘛。
//调试环境:XP sp2 + Delphi6 + Update2
//调试设置:关闭优化选项.
//主要是看看效率如何
//实际编程中应该如何应用,如何避免出现形参声明混乱的情况呢。
//暂且将过程名A,B,C 叫为A模式,B模式,C模式
//注:下面仅是对Integer形参的分析.
procedure A(I: Integer);
var
J: Integer;
begin
J := I;
I := 11;
end;
procedure B(var I: Integer);
var
J: Integer;
begin
J := I;
I := 12; // 由于对 Var 类型的I 复制,因此无需关闭优化选项
end;
procedure C(const I: Integer);
var
J: Integer;
begin
J := I;
//I := 12; // Could not compile
end;
procedure TForm1.btnTestVarClick(Sender: TObject);
var
Ka: Integer;
Kb: Integer;
Kc: Integer;
begin
Ka := 11;
Kb := 12;
Kc := 13;
A(Ka);
B(Kb);
C(Kc);
Log('Ka' + IntToStr(Ka));
Log('Kb' + IntToStr(Kb));
Log('Kc' + IntToStr(Kc));
end;
//Delphi局部变量的生成特点:
//存入堆栈,Ka,Kb,Kc在堆栈中的地址方向为:从高地址到低地址
0047D084 C745F80B000000 mov [ebp-$08],$0000000b
0047D08B C745F40C000000 mov [ebp-$0c],$0000000c
0047D092 C745F00D000000 mov [ebp-$10],$0000000d
0047D099 8B45F8 mov eax,[ebp-$08]
0047D09C E86FFFFFFF call A
0047D0A1 8D45F4 lea eax,[ebp-$0c] //B Var取得是地址
0047D0A4 E883FFFFFF call B
0047D0A9 8B45F0 mov eax,[ebp-$10]
0047D0AC E89BFFFFFF call C
0047D0B1 8D55E4 lea edx,[ebp-$1c]
procedure A(I: Integer);
0047CD28 55 push ebp
0047CD29 8BEC mov ebp,esp
0047CD2B 83C4F8 add esp,-$08
0047CD2E 8945FC mov [ebp-$04],eax //在堆栈中开辟一空间存放形参I值
0047CD31 8B45FC mov eax,[ebp-$04]
0047CD34 8945F8 mov [ebp-$08],eax
0047CD37 C745FC0B000000 mov [ebp-$04],$0000000b
0047CD3E 59 pop ecx
0047CD3F 59 pop ecx
0047CD40 5D pop ebp
0047CD41 C3 ret
procedure B(var I: Integer);
0047CD44 55 push ebp
0047CD45 8BEC mov ebp,esp
0047CD47 83C4F8 add esp,-$08
0047CD4A 8945FC mov [ebp-$04],eax //在堆栈中开辟一空间存放传入的地址
0047CD4D 8B45FC mov eax,[ebp-$04]
0047CD50 8B00 mov eax,[eax] //多了一次寻址
0047CD52 8945F8 mov [ebp-$08],eax
0047CD55 8B45FC mov eax,[ebp-$04] //多了一次寻址
0047CD58 C7000C000000 mov [eax],$0000000c
0047CD5E 59 pop ecx
0047CD5F 59 pop ecx
0047CD60 5D pop ebp
0047CD61 C3 ret
procedure C(const I: Integer);
0047CD64 55 push ebp
0047CD65 8BEC mov ebp,esp
0047CD67 83C4F8 add esp,-$08
0047CD6A 8945FC mov [ebp-$04],eax //在堆栈中开辟一空间存放形参I值
0047CD6D 8B45FC mov eax,[ebp-$04]
0047CD70 8945F8 mov [ebp-$08],eax
0047CD73 59 pop ecx
0047CD74 59 pop ecx
0047CD75 5D pop ebp
0047CD76 C3 ret
说明如下
//对整型形参而言,
A, C的执行代码效率是比较高的,
A比较简洁,C会增加编译器处理Const的时间
A与C除了const外,内部执行过程没有任何不同。
B 用于在函数内部修改外部变量的情况.
具体如何使用呢,我想看看SysUtils.pas,StrUtils如何处理的.
我对SysUtils,StrUtils走马观花看了一下,
在SysUtils中,
大部分函数的形参,如果是基本类型,比如Integer,Byte 基本上是按照A模式.
如果形参是String,全部是B模式常量声明(没有看到没有用const的)。
比如:
function StrToInt(const S: string): Integer;
function StrToIntDef(const S: string; Default: Integer): Integer;
function TryStrToInt(const S: string; out Value: Integer): Boolean;
function IntToStr(Value: Integer): string; overload;
function IntToStr(Value: Int64): string; overload;
function TrimLeft(const S: string): string; overload;
function TrimLeft(const S: WideString): WideString; overload;
一些特例:
procedure ScanToNumber(const S: string; var Pos: Integer);
function ScanChar(const S: string; var Pos: Integer; Ch: Char): Boolean;
function StrLCat(Dest: PChar; const Source: PChar; MaxLen: Cardinal): PChar;
function StrComp(const Str1, Str2: PChar): Integer;
在StrUtils中,
function ReverseString(const AText: string): string;
function LeftStr(const AText: string; const ACount: Integer): string;
function RightStr(const AText: string; const ACount: Integer): string;
function MidStr(const AText: string; const AStart, ACount: Integer): string;
System.pas中
procedure _WStrDelete(var S: WideString; Index, Count: Integer);
function StrPos(const Str1, Str2: PChar): PChar; assembler;
function StrUpper(Str: PChar): PChar; assembler;
function StrLower(Str: PChar): PChar; assembler;
function StrPas(const Str: PChar): string;
关于形参声明形式的个人小总结
相信 Delphi 中的 System,SysUtils,StrUtils 中函数形参的使用,都有其用意.
站在巨人的肩膀上,在实际开发中,声明大部分普通函数的形参的原则为:
如果是string格式的一定要加上const,使用C模式.
如果是基本类型如Integer,则使用A模式,什么也不加.
如果考虑到一些特殊的需求,比如传输内存,含有字符串删除等操作, 则考虑使用var声明.
还有许多没有解决或没有被证明的问题:
1、为什么string最好声明为const
2、const 的作用 等等。
后记:
之后,看了下 <<Pascal精要>> 第六章 过程与函数,觉得有些地方还是需要列出来以供参考:
1、
"Pascal 例程的传递参数可以是值参也可以是引用参数。
值参传递是缺省的参数传递方式:即将值参的拷贝压入栈中,例程使用、操纵的是栈中的拷贝值,不是原始值。
当通过引用传递参数时,没有按正常方式把参数值的拷贝压栈(避免拷贝值压栈一般能加快程序执行速度),
而是直接引用参数原始值,例程中的代码也同样访问原始值,这样就能在过程或函数中改变参数的值。
"
2、
参数引用技术在大多数编程语言中都有,C语言中虽没有,但C++中引入了该技术。
在C++中,用符号 &表示引用;在VB中,没有ByVal 标示的参数都为引用。
3、
通过引用传递参数对有序类型、传统字符串类型及大型记录类型才有意义。
实际上Delphi总是通过值来传递对象,因为Delphi对象本身就是引用。
因此通过引用传递对象就没什么意义(除了极特殊的情况),因为这样相当于传递一个引用到另一个引用。
Delphi 长字符串的情况略有不同,长字符串看起来象引用,但是如果你改变了该字符串的串变量,
那么这个串在更新前将被拷贝下来。
作为值参被传递的长字符串只在内存使用和操作速度方面才象引用,
但是如果你改变了字符串的值,初始值将不受影响。
相反,如果通过引用传递长字符串,那么串的初始值就可以改变。
[
本帖最后由 skyjacker 于 2007-2-2 11:46 编辑 ]