可变参数列表解析
可变参数是通过将函数实现为可变参数的形式,可以使得函数可以接受一个及一个以上的函数参数。也许可变参数对我们来说是陌生的,但是作为C语言的初学者经常使用的printf 函数函数是一个典型的参数可变的函数。在保证它的第一个参数是字符串的条件下,你可以输任意数量任意合法类型的参数。只要你在第一个字符串参数中使用了对应的格式化字符串,你就可以输出正确的值。printf函数是封装好的函数,那我们该如何实现一个可变参数函数呢?
1. 如何设定可变参数
首先我们通过一个列子来感受一下可变参数:实现一个函数可以找出任意个数里的最大值。
# include <stdio.h>
# include <stdlib.h>
# include <stdarg.h>
int Max(int n, ...)
{
va_list arg;
int i = 0;
va_start(arg, n);
int max = va_arg(arg, int);
for (i = 1; i < n; i++)
{
int tmp = va_arg(arg, int);
if (tmp>max)
max = tmp;
}
return max;
va_end(arg);
}
int main()
{
int max = Max(5, 7, 9, 6, 2, 1);
printf("max=%d\n", max);
system("pause");
return 0;
}
<1> 创建一个可变参数函数需引头文件<stdarg.h>。
<2> va_list arg 声明一个va_list 类型的变量arg,它用于访问参数列表未确定的部分。
<3> va_start(arg,n) 初始化 arg 这个变量,它的第一个参数是一个va_list类型的变量名,第二个参数是省略号最后一个有名字的参数。初始化过程把arg变量设置为指向可变参数的第一个参数。
<4> va_arg(arg,int) 为了访问参数,需要使用va_arg,这个宏可以接受两个参数:va_list 变量和参数列表中下一个参数的类型。在这个列子中所有可变参数都是整型。va_arg 返回这个函数的值,并使用va_arg指向下一个可变参数。
<5> va_end(arg)当访问完毕最后一个可变参数之后,调用va_end,由此确保堆栈的正确恢复,如果没有正确使用va_end ,可能使程序瘫痪。
1. 可变参数在编译器中的处理
首先看一下VC++6.0中<stdarg.h>里的代码
typedef char * va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
下面我们解释这些代码的含义:
1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的
2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大小。(对Intel 80x86 机器来说就是要求每个变量的地址都是sizeof(int)的倍数。)
3、 va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start (ap, v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,就可以遍历所有可变参数。(在大多数C语言编译器中,参数进栈的顺序是由右向左的,因此参数进栈后,最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。)
4、va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
因此,现在再来看va_arg()的实现就应该心中有数了:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏做了两个事情,
①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。
5、va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再指向堆栈,设为空指针。防止以后的误操作。
3.可变参数的限制
<1>可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你想一开始就访问列表中间的参数,那是不行的。
<2>参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用va_start。
<3>这些宏是无法直接判断实际存在参数的数量。
<4>这些宏无法判断每个参数的类型。
<5>如果在va_arg中指定了错误的类型,那么后果是不可预测的。