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)。