• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

Delphi与VC如何实现变参函数,类似Format、sprintf的变长参数实现原理,va_list与Arra ...

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

几乎所有高级语言都实现了一个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;

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

 


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap