C语言中的变长参数

        C语言带有变长参数的机制,最常见的变长参数函数,一个是 printf,另一个就是 scanf 了。相信有人和我一样,很好奇其中的实现机制,本文就简要介绍变长参数的用法和实现原理。主要参考《程序员的自我修养》一书11.2节的内容。

  • 变长参数的用法
  •         变长参数函数的声明方法很简单,可选参数一律用英文省略号“…”表示。比如 printf 的声明:

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

    以上声明表示,printf 除了第一个参数为确定的 const char *类型外,之后可以带有任意数量、任意类型的参数。在具体实现的时候,可以使用 stdarg.h 中定义的几个宏来依次获取各个额外参数。首先需要声明一个 va_list 类型的变量来指向可变参数序列,比如声明一个 ap 变量:

    va_list ap;
    

            声明上述变量后用宏 va_start 对其进行初始化,初始化需要借助于参数列表中最后一个确定函数,对 printf 来说,就是上面的 const char * format,所以对 ap 的初始化为:

    va_start(ap, format);
    

            初始化以后,ap 就指向 format 之后的第一个参数。初始化完成后,就可以用宏 va_arg 来获得下一个不定参数,前提是要先知道参数的类型。比如,下一个参数是 int 型,那么获取该参数就用:

    int next = va_arg(ap, int);
    

            在所有的参数取完之后,还要用宏 va_end 对 ap 清零,做到有始有终:

    va_end(ap);
    
  • 变长参数的实现原理
  •         变长参数得以实现,主要取决于两点:一是 C 语言默认的 cdecl 调用惯例自右向左压栈(自左向右压栈会有问题么?);二是 cdecl 调用惯例规定参数由调用方清除,保证了参数的正确清除。

            问题来了,上面用到的宏该如何实现?最简单的一种实现如下所示:

    #define    va_list    char*
    #define    va_start(ap, arg)    (ap = (va_list)&arg+sizeof(arg))
    #define    va_arg(ap, t)    (*(t*)((ap+=sizeof(t)) - sizeof(t)))
    #define    va_end(ap)    (ap=(va_list)0)
    

    可以看出,va_start(ap,arg) 将 ap 指向 arg 后面一个位置;va_arg(ap,t) 根据 t 的取值返回类型为 t 的值,并将 ap 指向后面一个地址。va_end(ap) 直接将 ap 置为 0,而 va_list 的类型用了常见的 char *(或者 void * 也是一样)。

  • 变长参数宏
  •         除了函数的参数可变长之外,宏的参数也可以是变长的。在 GCC 下用 “##” 连接操作实现,在 MSVC 下可用 __VA_ARGS__ 编译器内置宏。例如:

    //GCC
    #define    printf(args...)    fprintf(stdout, ##args)
    
    //MSVC
    #define    printf(...)    fprintf(stdout, __VA_ARGS__)
    

    上面两个宏的作用是一样的,都会把 printf(“%d”, 123) 展开成为 fprintf(stdout, “%d”, 123)。

发表评论

电子邮件地址不会被公开。 必填项已用*标注