Delphi与VC如何实现变参函数,类似Format、sprintf的变长参数实现原理,va_list与Array of const?

几乎所有高级语言都实现了一个format函数用于处理不同类型的数据组合转换为字符串。

delphi中有format,FormatBuf,FmtStr等,VC中有sprintf,CString中的format等,都是相当常用且方便的函数。

这些函数使用起来与普通函数最大的区别就是其中一个参数的个数、类型、是不确定的,反过来说,就是参数是可变的,这个特点使得该类函数功能变得异常强大,当然也极其方便。我们如何在自己的代码中实现类似这样的函数呢?

1.VC实现变参函数

这个特殊的参数在VC中使用“...”表示,下面先来一段代码:

using namespace std;

DWORD AddMsgf(char* sMsgFormat, ...)

{

char sBuffer[1024]={0};

va_list argp;

va_start (argp, sMsgFormat); /* 将可变长参数转换为va_list */

/* 将va_list传递给子函数 */

int iRLen = vsprintf_s(sBuffer, 512, sMsgFormat, argp);

va_end (argp);

//下面可以忽略

int iLen = strlen(sBuffer);

string sTmp(sBuffer, iLen);

DWORD dwRet = AddDebugMsg(sTmp, false);

return dwRet;

}

//s上面函数实际上并不高级,因为其实是借助vsprintf_s实现的,但是其原理已经差不多清晰了。

首先声明一个可以处理sMsgFormat的va_list数组。

再通过 va_start宏将你传进该函数的参数变量取得并存到va_list数组中;

这里面的sMsgFormat 作用就是告诉va_start如何取得sMsgFormat后面的所有参数变量地址的。

而参数是如何取得呢?

正常来说,我们使用函数参数无非就是直接使用该变量,而实际上对于CPU和进程来说,是通过将参数推入栈后,通过栈顶指针来取得具体参数地址或参数值,我们能够直接使用参数变量是因为编译器已经给你处理好了,而va_start就是在做编译器帮你处理好的工作,假如函数声明时“...”不是作为第二个参数,而是第三个,同时第二个参数是其他,比如下面这样:

DWORD AddMsgf(char* sMsgFormat,DWORD dwVal, ...),那我们应该如何处理呢?

这是va_start宏传入的就是 dwVal,而不是 sMsgFormat了,通常情况下因为32位CPU的地址对齐功能会使得一个参数地址只占4字节,所以使用va_start宏是能够满足几乎所有情况的,而C语言如果在其他16位机子或者嵌入式开发时需要具体根据情况决定了,但是原理就是根据参数"..."前面一个参数的地址,获得后面整个栈的地址内容存放到va_list数组中。

参数取得后,这里偷懒,直接使用vsprintf_s处理了,因为vsprintf_s接受va_list参数,同时内部会根据sMsgFormat来取得实际参数,如遇到%d,就取4字节内容,遇到%s,就取连续字符直到0x0。

原理就是如此, 你也可以自己通过处理 va_list里存储的参数来实现自己的函数功能,那时候就不需要 vsprintf_s,而是完完整整属于自己的变参函数,最后处理完成不要忘了使用 va_end释放数组。

2.Delphi实现变参函数

delphi实现变参函数是通过Array of const类型的参数来实现的,这个类型是一个TVarRec数组。

TVarRec 结构体是实现类似泛型变量功能的结构体的,具体如下:

PVarRec = ^TVarRec;

TVarRec = record { do not pack this record; it is compiler-generated }

case Byte of

vtInteger: (VInteger: Integer; VType: Byte);

vtBoolean: (VBoolean: Boolean);

vtChar: (VChar: Char);

vtExtended: (VExtended: PExtended);

vtString: (VString: PShortString);

vtPointer: (VPointer: Pointer);

vtPChar: (VPChar: PChar);

vtObject: (VObject: TObject);

vtClass: (VClass: TClass);

vtWideChar: (VWideChar: WideChar);

vtPWideChar: (VPWideChar: PWideChar);

vtAnsiString: (VAnsiString: Pointer);

vtCurrency: (VCurrency: PCurrency);

vtVariant: (VVariant: PVariant);

vtInterface: (VInterface: Pointer);

vtWideString: (VWideString: Pointer);

vtInt64: (VInt64: PInt64);

end;

该类型看起来成员非常多,其实实际数据只有2个成员,

record中使用cases是实现类型VC结构体中union联合体的功能的,

注意到其中一行:

vtInteger: (VInteger: Integer; VType: Byte);

其他成员都是与VInteger成员使用同一个地址的,而且大小都不大于 Integer类型。

这一行代码中第二个成员是这个结构体的关键成员:VType成员,该成员的值由编译器内部实现赋值的。

VType成员的值对应着case Byte of 中的Byte取值,具体有:

vtInteger = 0;

vtBoolean = 1;

vtChar = 2;

vtExtended = 3;

vtString = 4;

vtPointer = 5;

vtPChar = 6;

vtObject = 7;

vtClass = 8;

vtWideChar = 9;

vtPWideChar = 10;

vtAnsiString = 11;

vtCurrency = 12;

vtVariant = 13;

vtInterface = 14;

vtWideString = 15;

vtInt64 = 16;

以上几乎包括了Delphi所有基本数据类型,也就是说,根据VType,我们可以直接判断数组成员的数据类型。

看到这里,应该可以联系到VC中的va_list,该 TVarRec数组其实与VC中的va_list功能是类似的。

VC中的va_list的成员数据类型需要由 va_start函数来实现赋值,因此假如你传入的参数个数与字符串sMsgFormat中的%个数不一致,会导致内存错误。如你sprintf使用的参数是“Test%d,%d”,而实际变长参数却只传入一个1,那么 vsprintf_s根据 “Test%d,%d”会去栈里取第二个"%d"的参数,而实际中栈却只有一个参数,结果直接导致栈指针超过了,这时栈顶指针已经错误了,将会导致后面所有代码执行报错,造成栈内存溢出。

而Delphi则由编译器自动实现代码处理好的,不是根据“Test%d,%d”取参数的,因此安全很多,具体可以查看CPU代码,这里就不贴了,Deilphi直接在传入参数之前一步就避免了通过栈来取参数的麻烦,因为传的是Array of const,是一个动态数组,对于该函数来说其实就是一个指针而已,因此,我们直接处理 Array of const会比VC处理 va_list相对来说方便一些。

下面是一段代码:

function AddMsgF(param:Array of const):string;

var

i: integer;

begin

result := '';

if Length(param)=0 then Exit;

for I:=0 to High(param) do

with param[I] do

case VType of

vtInteger: result := result + inttostr(VInteger);

vtBoolean: result := result + Booltostr(VBoolean, True);

vtChar: result := result + VChar;

//下面其他类型不做处理了

//vtExtended: (VExtended: PExtended);

//vtString: (VString: PShortString);

//vtPointer: (VPointer: Pointer);

//vtPChar: (VPChar: PChar);

//vtObject: (VObject: TObject);

//vtClass: (VClass: TClass);

//vtWideChar: (VWideChar: WideChar);

//vtPWideChar: (VPWideChar: PWideChar);

//vtAnsiString: (VAnsiString: Pointer);

//vtCurrency: (VCurrency: PCurrency);

//vtVariant: (VVariant: PVariant);

//vtInterface: (VInterface: Pointer);

//vtWideString: (VWideString: Pointer);

//vtInt64: (VInt64: PInt64);

end;

end;

其实实现这种函数不是难点,因为难点都被人实现并封装好了,或者已经被编译器处理好了,这些与其说实现,不如说使用,不过知道如何使用,也算是一种技术吧。