zoukankan      html  css  js  c++  java
  • 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;

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

  • 相关阅读:
    当下流行的分布式文件系统大阅兵
    smb相关资料
    Linux下将多个静态库(.a)合并成一个静态库文件(.a)的命令操作,方法一
    IBInspectable的使用
    iOS开发拓展篇——如何把项目托管到GitHub
    iOS开发拓展篇-XMPP简单介绍
    iOS开发拓展篇—应用之间的跳转和数据传递
    使用NSURLSession获取网络数据和下载文件
    李洪强实现横向滚动的View<二>
    李洪强实现横向滚动的View<一>
  • 原文地址:https://www.cnblogs.com/caibirdy1985/p/4232972.html
Copyright © 2011-2022 走看看