C语言可变参数函数取参方法

On February 26, 2012, in 语言学习, C语言, by sponge

熟悉C的人都知道,C语言支持可变参数函数(Variable Argument Functions),即参数的个数可以是不定个,在函数定义的时候用(...)表示,比如我们常用的printf()\execl函数等;printf函数的原型如下:

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

注意,采用这种形式定义的可变参数函数,至少需要一个普通的形参,比如上面代码中的*format,后面的省略号是函数原型的一部分。

C语言之所以可以支持可变参数函数,一个重要的原因是C调用规范中规定C语言函数调用时,参数是从右向左压入栈的;这样一个函数实现的时候,就无需关心调用他的函数会传递几个参数过来,而只要关心自己用到几个;以printf为例:

printf("%d%s\n",i,s);

printf函数在定义的时候,不知道函数调用的时候会传递几个参数。在实现上,printf函数只需关心第一个参数,即字符串“%d%s\n”,当读到%d的时候,printf知道自己需要第二个参数,这时只需要去栈上寻找即可;当读到%s时,再去栈上网上寻找一个参数即可。简单说,printf不关心栈上到底压了多少参数,只关心自己需要多少。

那么对于一个定义为可变参数的函数,函数定义的时候并没有定义形参原型,怎么使用参数呢?

C语言定义了一系列宏来完成可变参数函数参数的读取和使用:宏va_start、va_arg和va_end;在ANSI C标准下,这些宏定义在stdarg.h中。三个宏的原型如下:

void va_start(va_list ap, last);//取第一个可变参数(如上述printf中的i)的指针给ap,last是函数声明中的最后一个固定参数(比如printf函数原型中的*fromat);
type va_arg(va_list ap, type);//返回当前ap指向的可变参数的值,然后ap指向下一个可变参数;type表示当前可变参数的类型(支持的类型位int和double);
void va_end(va_list ap);//将ap置为NULL

当一个函数被定义位可变参数函数时,其函数体内首先要定义一个va_list的结构体类型,这里沿用原型中的名字,ap。

va_start使ap指向第一个可选参数。va_arg返回参数列表中的当前参数并使ap指向参数列表中的下一个参数。va_end把ap指针清为NULL。函数体内可以多次遍历这些参数,但是都必须以va_start开始,并以va_end结尾。

下面是一个具体的示例(摘自wikipedia):

#include <stdarg.h>
 
double average(int count, ...)
{
    va_list ap;
    int j;
    double tot = 0;
    va_start(ap, count); //使va_list指向起始的參數
    for(j=0; j<count; j++)
        tot+=va_arg(ap, double); //檢索參數,必須按需要指定類型
    va_end(ap); //釋放va_list
    return tot/count;
}

除此之外,我们还需要注意一个陷阱,即va_arg宏的第2个参数不能被指定为char、short或者float类型。《C和C++经典著作:C陷阱与缺陷在可变参数函数传递时,因为char和short类型的参数会被提升为int类型,而float类型的参数会被提升为double类型 。

例如,以下的代码是错误的

a = va_arg(ap,char);

因为我们无法传递一个char类型参数,如果传递了,它将会被自动转化为int类型。上面的式子应该写成:

a = va_arg(ap,int);

还需要注意的一个问题是,即时我们知道在某种体系结构下C语言函数的参数都压在栈上,我们也应该避免直接去栈上取想要的参数,因为这样会降低程序的灵活性和可移植性,并带来一些安全上潜在的危险。上述的三个宏,包括va_list,在不同的体系结构下会有不同的实现方法,比如va_list,有的系统上直接指向栈;而有的系统却将其实现为一个指针数组。

参考资料:
UNIX环境高级编程(第2版)
C和C++经典著作:C陷阱与缺陷

anyShare分享到:
Tagged with:
 

12 Responses to “C语言可变参数函数取参方法”

  1. littlecat says:

    学习了,继续加油!

  2. wildpointer says:

    关于a = va_arg(ap,char);,我有不同的看法。
    我看了一下C标准,并没有提第二个参数不能用char, short。
    在VS2010 Express 32bit 的编译器上试了一下下面的小程序,是可以正确运行的:
    #include
    #include
    void read_char(int i, ...)
    {
    char c;
    va_list ap;
    va_start(ap, i);
    while((c = va_arg(ap, char)) != 0)
    {
    printf("%c", c);
    }
    va_end(ap);
    }

    int main()
    {
    read_char(0, 'a', 'b', 'c', '');
    }
    也就是说,不管从标准角度还是从实现的角度,都可以把第二个参数设成char。

    我看了一下VS2010 Express 32bit的va_*宏的实现,发现它们会自动对齐到int的边界。当然,va_*宏的实现肯定和calling convention有关。

    typedef char * va_list;

    #define va_start _crt_va_start
    #define va_arg _crt_va_arg
    #define va_end _crt_va_end

    //下面这个宏就是用来对齐的。
    #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
    #define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
    #define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
    #define _crt_va_end(ap) ( ap = (va_list)0 )

    C陷阱与缺陷的一些东西可能过时了。

    • wildpointer says:

      代码的格式乱了。在main函数中,read_char的最后一个参数是字符0。

      • sponge says:

        @wildpointer

        1、首先赞仔细研究的精神,在某些编译器下char类型确实能被对齐到4字节。
        2、C标准中确实没有提到第二个参数不能是char,这也正是陷阱存在的地方,如果C语言中提到第二个参数可以使用char,那么所有的C编译器就必须严格执行4字节对齐;但是没有提到,就意味着编译器可做可不做。
        3、C标准对参数提升规则是有明确说明的,但对va_arg自动对齐却没有说明;也就是说自动对齐工作,编译器可做可不做。
        4、在gcc 3.4.5 以及任何以前的版本(以后的版本没试验),你编译这段代码,会报以下错误:
        1.c: In function `read_char':
        1.c:8: warning: `char' is promoted to `int' when passed through `...'
        1.c:8: warning: (so you should pass `int' not `char' to `va_arg')
        1.c:8: note: if this code is reached, the program will abort
        1.c:17:30: empty character constant

  3. [...] Liu的这篇文章之后,学到了参数类型提升相关的东西。 [...]

  4. jimohou says:

    最近需要了解一些C语言的东西,你的文章给了我很多helps.~

  5. 冰上游鱼 says:

    虽然不常用,但是变参还是很有用的。

  6. gong says:

    你好,对于字符串类型参数,va_arg(argp, type)这个宏里面的type也就是char*,而sizeof(char*)=sizeof(int),又怎么能保证这个宏执行完argp指向下一个可变参数地址呢?困扰了很久了

  7. gong says:

    你好,对于字符串类型参数,va_arg(argp, type)这个宏里面的type也就是char*,而sizeof(char*)=sizeof(int)=4,又怎么能保证这个宏执行完argp指向下一个可变参数地址呢?困扰了很久了

  8. sony says:

    有什么办法可以实现可变参数接受struct

Leave a Reply

Note: Commenter is allowed to use '@User+blank' to automatically notify your reply to other commenter. e.g, if ABC is one of commenter of this post, then write '@ABC '(exclude ') will automatically send your comment to ABC. Using '@all ' to notify all previous commenters. Be sure that the value of User should exactly match with commenter's name (case sensitive).