相对于固定参数的函数,变参函数的可用性无疑是更好的。我们最常用的变参函数包括scanf和printf。刚刚接触到变参函数的时候,我觉得这太神奇了,它并不知道我要输入什么类型的数据,要输入多少个数据,却能完美地处理。其实,可变参数机制实现起来是相当容易的(在stdarg.h的基础上),而且,它的作用并没有想象中的那么神奇。
可变参数机制并不能获取某次输入的所有参数的个数,也不能自己确定每一个输入参数的类型。嗯,没错,看上去printf和scanf就能知道每次输入的参数个数和每个参数的类型。其实,仔细想一想就会发现printf和scanf没这个本事,输入的参数个数和每个参数的类型是使用者在format内容中,通过%模式等告诉编译器的。光是这么说可能不够取信于人,以简化的printf为例,让我们看一段K&R中的例程吧:
#include <stdarg.h> /* minprintf: minimal printf with variable argument list */ void minprintf(char *fmt, ...) { va_list ap; /* points to each unnamed arg in turn */ char *p, *sval; int ival; double dval; va_start(ap, fmt); /* make ap point to 1st unnamed arg */ for (p = fmt; *p; p++) { if (*p != '%') { putchar(*p); continue; } switch (*++p) { case 'd': ival = va_arg(ap, int); printf("%d", ival); break; case 'f': dval = va_arg(ap, double); printf("%f", dval); break; case 's': for (sval = va_arg(ap, char *); *sval; sval++) putchar(*sval); break; default: putchar(*p); break; } } va_end(ap); /* clean up when done */ } |
可以看出,printf处理可变参数的关键就在于它的参数char *fmt。注意程序中的for循环,指针p正是一次次地根据fmt中的提示(在这个简化的例子中是%)经由switch分支来确定下一个参数的类型和有效参数的个数。
所以说,可变参数不是万能的,它只是一种很normal的机制,不过正是先驱们那化腐朽为神奇的想象力,借由这种normal的机制实现了神奇而又令人诟病的scanf和printf函数。一般而言,除了像printf一样,在函数中实现一个特殊的format之外,一些可变参数中的参数类型都是一致的(比如说只可能都是int)函数,则会在之前的固定参数中用一个参数指出可变参数的个数或是类型。下面是一个C Primer Plus中的例程:
#include <stdarg.h> double sum(int lim,...) { va_list ap; // declare object to hold arguments double tot = 0; int i; va_start(ap, lim); // initialize ap to argument list for (i = 0; i < lim; i++) tot += va_arg(ap, double); // access each item in argument list va_end(ap); // clean up return tot; } |
可以看出,函数sum中的固定参数lim指出了可变参数的个数。
以上介绍了可变参数机制实现的两种过程:自定义format和前置指示标志。接下来就会详细解释在C语言中如何具体实现可变参数机制。从两个例程可以看出,有四个宏是至关重要的:
va_list va_start(va_list, lastpar) va_arg(va_list, Type) va_end(va_list) |
va_list:一个char链表(实际上应该是一个连续的内存块,像数组一样),在使用时表现为一个指向char类型的指针;
va_start:初始化va_list。通过最后的固定参数实现对可变参数初始位置的定位,并为va_list分配内存,将可变参数复制该内存块中,使va_list指向该内存块的初始位置;
va_arg:通过移动指针va_list获取由参数Type指定的变量并返回该变量。
va_end:释放va_list拥有的内存块所占据的内存空间。
看,一切不就一清二楚了吗?不过,还有如下几个问题还需要特别注意一下:
1> C标准规定实现可变参数机制的函数至少要有一个固定参数。从上面的讨论可以看出,这无论是从语法上还是实现上都是必须的。
2> 隐式类型转换不可用。比如说,va_arg指定了类型是double,若传入的是int的变量10就会出错。
3> C语言的整型提升原则。也就是说,在va_arg中获取float和double使用的Type都是double,而获取char、short和int使用的Type都是int。
函数指针不可用,除非用 typedef 定义过。