函数参数表中的三个点表示什么

标准库提供的一些参数的数目可以有变化的函数。例如我们很熟悉的printf,它需要有一个格式串,还应根据需要为它提供任意多个“其他参数”。这种函数被称作“具有变长度参数表的函数”,或简称为“变参数函数”。我们写程序中有时也可能需要定义这种函数。要定义这类函数,就必须使用标准头文件<stdarg.h>,使用该文件提供的一套机制,并需要按照规定的定义方式工作。本节介绍这个头文件提供的有关功能,它们的意义和使用,并用例子说明这类函数的定义方法。
一个变参数函数至少需要有一个普通参数,其普通参数可以具有任何类型。在函数定义中,这种函数的最后一个普通参数除了一般的用途之外,还有其他特殊用途。下面从一个例子开始说明有关的问题。
假设我们想定义一个函数sum,它可以用任意多个整数类型的表达式作为参数进行调用,希望sum能求出这些参数的和。这时我们应该将sum定义为一个只有一个普通参数,并具有变长度参数表的函数,这个函数的头部应该是(函数原型与此类似):
int sum(int n, ...)
我们实际上要求在函数调用时,从第一个参数n得到被求和的表达式个数,从其余参数得到被求和的表达式。在参数表最后连续写三个圆点符号,说明这个函数具有可变数目的参数。凡参数表具有这种形式(最后写三个圆点),就表示定义的是一个变参数函数。注意,这样的三个圆点只能放在参数表最后,在所有普通参数之后。
为了能在变参数函数里取得并处理不定个数的“其他参数”,头文件<stdarg.h>提供了一套机制。这里提供了一个特殊类型va_list。在每个变参数函数的函数体里必须定义一个va_list类型的局部变量,它将成为访问由三个圆点所代表的实际参数的媒介。下面假设函数sum里所用的va_list类型的变量的名字是vap。在能够用vap访问实际参数之前,必须首先用“函数”va_start做这个变量初始化。函数va_start的类型特征可以大致描述为:
va_start(va_list vap, 最后一个普通参数)
实际上va_start通常并不是函数,而是用宏定义实现的一种功能。在函数sum里对vap初始化的语句应当写为:
va_start(vap, n);
在完成这个初始化之后,我们就可以通过另一个宏va_arg访问函数调用的各个实际参数了。宏va_arg的类型特征可以大致地描述为:
类型 va_arg(va_list vap, 类型名)
在调用宏va_arg时必须提供有关实参的实际类型,这一类型也将成为这个宏调用的返回值类型。对va_arg的调用不仅返回了一个实际参数的值(“当前”实际参数的值),同时还完成了某种更新操作,使对这个宏va_arg的下次调用能得到下一个实际参数。对于我们的例子,其中对宏va_arg的一次调用应当写为:
v = va_arg(vap, int);
这里假定v是一个有定义的int类型变量。
在变参数函数的定义里,函数退出之前必须做一次结束动作。这个动作通过对局部的va_list变量调用宏va_end完成。这个宏的类型特征大致是:
void va_end(va_list vap);
下面是函数sum的完整定义,从中可以看到各有关部分的写法:
int sum(int n, ...) {
va_list vap;
int i, s = 0;
va_start(vap, n);
for (i = 0; i < n; i++) s += va_arg(vap, int);
va_end(vap);
return s;
}
这里首先定义了va_list变量vap,而后对它初始化。循环中通过va_arg取得顺序的各个实参的值,并将它们加入总和。最后调用va_end结束。
下面是调用这个函数的几个例子:
k = sum(3, 5+8, 7, 26*4);
m = sum(4, k, k*(k-15), 27, (k*k)/30);
在编写和使用具有可变数目参数的函数时,有几个问题值得注意。首先,虽然在上面描述了头文件所提供的几个宏的“类型特征”,实际上这仅仅是为了说明问题。因为实际上我们没办法写出来有关的类型,系统在预处理时进行宏展开,编译时即使发现错误,也无法提供关于这些宏调用的错误信息。所以,在使用这些宏的时候必须特别注意类型的正确性,系统通常无法自动识别和处理其中的类型转换问题。
第二:调用va_arg将更新被操作的va_list变量(如在上例的vap),使下次调用可以得到下一个参数。在执行这个操作时,va_arg并不知道实际有几个参数,也不知道参数的实际类型,它只是按给定的类型完成工作。因此,写程序的人应在变参数函数的定义里注意控制对实际参数的处理过程。上例通过参数n提供了参数个数的信息,就是为了控制循环。标准库函数printf根据格式串中的转换描述的数目确定实际参数的个数。如果这方面信息有误,函数执行中就可能出现严重问题。编译程序无法检查这里的数据一致性问题,需要写程序的人自己负责。在前面章节里,我们一直强调对printf等函数调用时,要注意格式串与其他参数个数之间一致性,其原因就在这里。
第三:编译系统无法对变参数函数中由三个圆点代表的那些实际参数做类型检查,因为函数的头部没有给出这些参数的类型信息。因此编译处理中既不会生成必要的类型转换,也不会提供类型错误信息。考虑标准库函数printf,在调用这个函数时,不但实际参数个数可能变化,各参数的类型也可能不同,因此不可能有统一方式来描述它们的类型。对于这种参数,C语言的处理方式就是不做类型检查,要求写程序的人保证函数调用的正确性。
假设我们写出下面的函数调用:
k = sum(6, 2.4, 4, 5.72, 6, 2);
编译程序不会发现这里参数类型不对,需要做类型转换,所有实参都将直接传给函数。函数里也会按照内部定义的方式把参数都当作整数使用。编译程序也不会发现参数个数与6不符。这一调用的结果完全由编译程序和执行环境决定,得到的结果肯定不会是正确的。