• C和C++中的可变参数及宏的使用


    可变参数

    可变参数即表示参数的个数可以变化,可多可少,也表示参数的类型也可以变化,可以是int,double还可以是char*,类,结构体等等。printf()sprintf()等函数的实现就使用了可变参数,可变参数还可以用来对任意数量的数据进行求和、求平均值,非常方便(不然就用数组或每种写个重载)。在C#中有专门的关键字parame来实现可变参数,但在C,C++并没有类似的语法,不过幸好提供这方面的处理函数,下面将重点介绍如何使用这些函数。

    可变参数表示

    用三个点来表示,例如printf()函数和scanf()函数的声明:

    int printf(const char *, ...);
    int scanf(const char *, ...);

    形参列表里,第一个是固定参数,后面的三个点就表示可变参数。

    这三个点用在宏中就是变参宏(Variadic Macros),默认名称为__VA_ARGS__。如:

    #define WriteLine(...) { printf(__VA_ARGS__); putchar('
    ');}

    WriteLine("MoreWindows");

    考虑下printf()的返回值是表示输出的字节数。将上面宏改成:

    #define WriteLine (...) printf(__VA_ARGS__) + (putchar('
    ') != EOF ? 1: 0);

    这样就可以得到WriteLine宏的返回值了,它将返回输出的字节数,包括最后的" "。如下例所示i和j都将输出12。

    int i = WriteLine("MoreWindows");
    WriteLine("%d", i);
    int j = printf("%s
    ", "MoreWindows"); 
    WriteLine("%d", j);

    如何处理va_list类型

    函数内部对可变参数都用va_list及与它相关的三个宏来处理,这是实现变参调用的关键之处。

    在<stdarg.h>中可以找到va_list的定义:

    typedef char *va_list;

    再介绍与它关系密切的三个宏要介绍下:va_start()va_end()va_arg()

    同样在<stdarg.h>中可以找到这三个宏的定义:

    #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
    #define va_end(ap)      ( ap = (va_list)0 )
    #define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

    _INTSIZEOF( )

    宏定义如下:

    #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

    ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ),看上去有点复杂,全是位操作,其实目的非常简单:就是取整到sizeof(int)。比如sizeof(int)为4,1,2,3,4就取4,5,6,7,8就取8。

    技巧对x向n取整用C语言的算术表达就是((x+n-1)/n)*n当n为2的幂时可以将最后二步运算换成位操作——将最低 n - 1个二进制位清 0就可以了

    分析:为什么是int的倍数?

    因为大多数编译器是push一次都是push一个DWORD(4字节),所以哪怕传的第一个是char,也会自动补成4字节。在VS里反汇编了一下,结果如下:

    f(c,i);
    00411539  mov       eax,dword ptr [i] 
    0041153C  push      eax 
    0041153D  movzx    ecx,byte ptr [c] 
    00411541  push       ecx 
    00411542  call         f (411023h) 
    00411547  add        esp,8 
    //可看出push 4字节的。

    va_end(ap)

    这个最简单,就是将指针置成NULL。

    va_start(ap,v)

    先取v的地址,再加上_INTSIZEOF(v)。

    va_arg(ap,t)

    从ap中取出类型为t的数据,并将指针相应后移一个单位。如va_arg(ap, int)就表示取出一个int数据并将指针向后移四个字节。

    小结

    因此在函数中先用va_start()得到变参的起始地址,再用va_arg()一个一个取值,最后再用va_end()收尾就可以解析可变参数了。

    vfprintf()函数和vsprintf()函数    

    vfprintf()这个函数很重要,光从名字上看就知道它与经常使用的printf()函数有很大的关联。它有多个重载版本,最常用的一种:

    //函数原型
    int vfprintf(
       FILE *stream,
       const char *format,
       va_list argptr
    );

    第一个参数为一个FILE指针。FILE结构在C语言的读写文件必不可少。要对屏幕输出传入stdout。

    第二个参数指定输出的格式。

    第三个参数是va_list类型,这个少见,但其实就是一个char *表示可变参参数的起始地址。

    返回值:成功返回输出的字节数(不包括最后的’’),失败返回-1。

    vsprintf()与上面函数类似,就只列出函数原型了:

    int vsprintf(
       char *buffer,
       const char *format,
       va_list argptr
    );

    还有一个int _vscprintf(const char *format, va_list argptr ); 可以用来计算vsprintf()函数中的buffer字符串要多少字节的空间。

    代码范例

    //实现的printf()函数(注1)与WriteLine()函数
    int Printf(char *pszFormat, ...) {
           va_list pArgList;
           va_start(pArgList, pszFormat);
           int nByteWrite = vfprintf(stdout, pszFormat, pArgList);
           va_end(pArgList);
           return nByteWrite;
    }
    
    int WriteLine(char *pszFormat, ...) {
           va_list   pArgList;
           va_start(pArgList, pszFormat);
           int nByteWrite = vfprintf(stdout, pszFormat, pArgList);
           if (nByteWrite != -1)
                  putchar('
    '); //注2
           va_end(pArgList);
           return (nByteWrite == -1 ? -1 : nByteWrite + 1);
    }

    调用与printf()函数相同。

    注1. 网上有不用vfprintf()自己解析参数来实现printf()的,但很少能将功能做到与printf()相近(实际上能完全熟悉printf()的人已经就不多,不信的话可以先看看《C陷阱与缺陷》了解printf()很多不太常用的参数,再去Microsoft Visual StudioVC98CRTSRC中查看OUTPUT.C对printf()的实现)。

    注2. 【技巧】如果输出单个字符 putchar(ch)会比printf(“%c”, ch)效率高的多。在字符串不长的情况下,多次调用putchar()也会比调用printf(“%s ”, szStr);的效率高。在函数大量调用时非常明显

    再给出一个用可变参数来求和,遗憾的在C/C++中无法确定传入的可变参数的个数(printf()中是通过扫描'%'个数来确实参数的个数的),因此要么就需指定个数,要么在参数的最后要设置哨兵数值:

    const int GUARDNUMBER = 0; //哨兵标识
    //变参参数的个数无法确定,在printf()中是通过扫描'%'个数,在这通过设置哨兵标识来确定变参参数的终止
    int MySum(int i, ...) {
           int sum = i;
           va_list argptr;
           va_start(argptr, i);
           while ((i = va_arg(argptr, int)) != GUARDNUMBER)  //和上面的+=类似
               sum += i;
           va_end(argptr);
           return sum;
    }

    可以这样的调用:   

    printf("%d
    ", MySum(1, 3, 5, 7, 9, 0));

    但不可以直接传入一个0: 

    printf("%d
    ", MySum(0)); //error
    //指定个数
    int MySum(int nCount, ...) {
           if (nCount <= 0)
                  return 0;
           int sum = 0;
           va_list argptr;
           va_start(argptr, nCount);
           for (int i = 0; i < nCount; i++)
                  sum += va_arg(argptr, int);
           va_end(argptr);
           return sum;
    
    }

    调用时第一个参数表示后面参数的个数如:

    printf("%d
    ", MySum(5, 1, 3, 5, 7, 9));
    printf("%d
    ", MySum(0));

    代码所用的头文件:

    #include <stdarg.h>

    #include <stdio.h>

    可变参数的使用方法远远不止上述几种,不过

    【注意】在C/C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的"%"符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃

    变参宏的使用详解

    VA_LIST及其成员宏单独拎出来再细讲下

    va_list型变量

    #ifdef  _M_ALPHA
    typedef struct{
    char* a0;  //pointer to first homed integer argument
    int offset; //byte offset of next parameter
    } va_list;
    #else
    typedef char* va_list;
    #endif

    _INTSIZEOF宏

    获取类型占用的空间长度,最小占用长度为int的整数倍:

    #define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))
    |-------------------------------------------------|  高地址
    |-------------------函数返回地址--------------------|
    
    |------------.........................------------|
    |-------------------------------------------------|<-- va_arg后ap指向
    
    |                第n个参数(第一个可变参数)          |
    |-------------------------------------------------|<-- va_start后ap指向
    
    |              第n-1个参数(最后一个固定参数)        |
    
    |-------------------------------------------------|<-- &v 低地址

    VA_START宏

    获取可变参数列表的第一个参数的地址(ap是类型为va_list的指针,v是可变参数最左边的参数,亦即最后一个固定参数):

    #define va_start(ap,v) (ap=(va_list)&v+_INTSIZEOF(v))

    VA_ARG宏

    获取可变参数的当前参数,返回指定类型并将指针指向下一参数(t参数描述了当前参数的类型):

    #define va_arg(ap,t) (*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))

    VA_END宏

    清空va_list可变参数列表:

    #define va_end(ap) (ap=(va_list)0)

    VA_LIST的用法

    (1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针;

    (2)然后用VA_START初始化变量刚定义的VA_LIST变量,使其指向第一个可变参数的地址;

    (3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数);

    (4)最后用VA_END宏结束可变参数的获取

    使用VA_LIST应该注意的问题:

    1. 可变参数的类型和个数完全由程序代码控制,它并不能智能地识别不同参数的个数和类型;
    2. 如果我们不需要一一详解每个参数,只需要将可变列表拷贝至某个缓冲,可用vsprintf函数
    3. 因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利。不利于我们写出高质量的代码

    小结

    可变参数的函数原理其实很简单,而VA系列是以宏定义来定义的,实现跟堆栈相关。我们写一个可变参数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数,如果在C++里,我们应该利用C++多态性来实现可变参数的功能,尽量避免用C语言的方式来实现。

    示例程序

    #include <iostream>
    #include <stdarg.h>
    using namespace std;
    
    int sum(char * msg, ...);
    int my_vsprintf(char *buf, char *format, ...);
    
    int main() {
        sum("The sum of the list is:", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0);
        cout << endl;
        char buf[256];
        my_vsprintf(buf, "%My name is %s and I am %d years old.", "Ben", 24);
        cout << buf << endl;
        system("pause");
        return 0;
    }
    
    int sum(char *msg, ...) {
        va_list st;
        va_start(st, msg);
        int total = 0;
        int tmp;
        while((tmp = va_arg(st, int)) != 0)
        {
            total += tmp;
        }
        va_end(st);
        cout << "The sum of the list is: " << total;
        return 0;
    }
    
    int my_vsprintf(char *buf, char *format, ...) {
        va_list st;
        va_start(st, format);
        vsprintf(buf, format, st);
        /***************************************************************************/
        /*       函数名: vsprintf
        /*       功 能: 送格式化输出到串中
        /*       返回值: 正常情况下返回生成字串的长度(除去),错误情况返回负值
        /*       用 法: int vsprintf(char *string, char *format, va_list param);
        /*                将param 按格式format写入字符串string中
        /*       注: 该函数会出现内存溢出情况,建议使用vsnprintf                                                                            */
        /***************************************************************************/
        va_end(st);
        return 0;
    }

     (整理自网络)

    参考资料:

    https://www.cnblogs.com/HenryThinker/archive/2012/12/14/2817370.html

    https://www.cnblogs.com/dongsheng/p/4001555.html

    Min是清明的茗
  • 相关阅读:
    python2和3切换时的几个注意点会报错
    Python异常UnicodeEncodeError 'gbk' codec can't encode character 'xa0'
    python爬虫使用Xpath爬取指定位置的内容
    问题账户需求分析
    2018年春季个人阅读计划
    我们应当怎样做需求分析
    人月神话读后感3
    人月神话读后感2
    线程池
    生产者消费者
  • 原文地址:https://www.cnblogs.com/MinPage/p/13969914.html
Copyright © 2020-2023  润新知