1 自定义可变参数的函数

C语言中的<stdarg.h>中提供了可变参数的函数。使用步骤如下:

  1. 在函数原型中使用省略号,且省略号必须是最后一个参数
  2. 在函数定义中创建一个va_list类型的变量
  3. 用宏va_start将该变量初始化为一个参数列表
  4. 用宏va_arg访问参数列表
  5. 用宏va_end完成清理工作

下面进行详细说明。

1 函数原型

需要注意的是函数原型必须具有一个参数列表:在C/C++中,任何使用变长参数声明的函数都必须至少有一个指定的参数(又称强制参数),即至少有一个参数的类型是已知的,而不能用三个点省略所有参数的指定,且已知的指定参数必须声明在函数最左端。其中,最右边的参量(省略号前)起着特殊作用:ANSI标准使用parmN表示该变量,传递给该参量的值将是省略号部分代表的参数个数。比如下面的定义:

void f1(int n, ...);    // OK
int f2(int n, const char *s, ...);  // OK
char f3(char c1, ..., char c2);    // Error
double f4(...);    // Error

2 va_list

va_list类型代表一种数据对象,该数据对象用于存放参量列表中省略号部分代表的参量。比如:

double sum(int lim, ...)
{
    va_list ap;    // 声明用于存放参数的变量
    ....
}

在这个例子中,lim为参量parmN,由它来指定可变参数列表中的参数个数。

3 va_start

我们使用va_start宏把参数列表复制到va_list变量中。va_start( )有两个参数:va_list类型的变量和参量ParmN。比如:

va_start(ap, lim);  // 把ap初始化为参数列表

4 va_arg

第一次调用va_arg( )时,它返回参数列表的第一项,下次调用时返回第二项,依此类推。该宏接受两个参数:一个va_list类型的变量和一个类型名,该类型为va_arg( )返回值的类型。实际的参数类型必须与说明的类型匹配,否则将出错。需要注意的是:在可变长参数中,应用的是"加宽"原则。也就是float类型被扩展成doublechar, short被扩展成int。因此,如果你要取可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double);charshort类型的则用va_arg(argp, int)

需要注意的是va_arg没有提供后退回先前参数的方法,所以C99提供了一个宏va_copy( ),该宏的参数为两个va_list类型的变量,作用是将第二个参数复制到第一个参数中。

5 va_end

该宏完成清理工作:比如释放动态分配的用于存放参数的内存。其参数为一个va_list类型的变量。

下面是一个完整的例子:

#include <stdio.h>
#include <stdarg.h>

double sum(int, ...);

int main(void)
{
	double s, t;

	s = sum(3, 1.1, 2.5, 13.3);
	t = sum(6, 1.1, 2.5, 13.3, 4.1, 5.1, 6.1);

	printf("s = %gn", s);
	printf("t = %gn", t);

	return 0;
}

double sum(int lim, ...)
{
	va_list ap;       // 声明用于存放参数的变量

	double tot = 0;
	int		i;
	
	va_start(ap, lim);   // 把ap初始化为参数列表

	for (i = 0; i < lim; i++)
		tot += va_arg(ap, double);    // 访问参数列表中的每一个项目

	va_end(ap);     // 清理工作

	return tot;
}

 2 可变参数是如何工作的

其实只要了解C语言的函数参数传递的机制,可变参数的工作原理就不难理解。我们知道函数调用过程中参数的传递是通过栈来实现的,而且是按照从右至左的顺序依次压栈的。也就是说,(函数调用时)当所有的参数都入栈以后,可变参数一定在栈底,而强制参数一定在栈顶。这样结合参数类型,我们就可以从栈顶依次找到每一个参数的位置,从而取到每一个参数,va_arg宏正是做了这样一件事(所以,传给该宏的参数类型一定要正确,该宏并不会做任何的隐式、显式的类型转换)。由此,我们也知道为何函数原型中一定要有一个强制参数了。

同时也需要注意到parmN的值一定不能大于可变参数的个数,否则将访问到栈以外的数据。而且并不是所有的变长参数函数都有一个类似parmN这样的说明参数个数的值的参数,但是为了安全起见,最好能有这样一个参数。如果没有,也必须有一定的方式使我们可以知道访问到了最后一个参数,比如将-1设为最后一个参数。因为va_arg宏并没有实现探测参数列表的结尾,这个需要我们在程序里面自己去实现。