1.在C中,变长参数的函数即参数个数可变、参数类型不定 的函数。当我们无法列出传递函数的所有实参的类型和数目时,以三个点结束该函数的声明,指定参数表.
int InitArray(Array &A,int dim,...);
// printf函数的声明
int printf(const char * _Format, ...);
//scanf函数声明
int scanf(const char * _Format, ...);
//自定义变长参数函数func的声明
int func(int a,int b,...);
在C/C++中,任何使用变长参数声明的函数都必须至少有一个指定的参数(又称强制参数),即至少有一个参数的类型是已知的,而不能用三个点省略所有参数的指定,且已知的指定参数必须声明在函数最左端。即int func(...);是非法的,int func(...,int a);是非法的。
2.含有变长参数的函数是怎么实现的呢?
变长参数函数的实现其实关键在于怎么使用参数,指定了的参数好说,直接使用指定的参数名称访问,但未指定的参数呢?
#include <stdio.h>
void foo(int x, int y, int z)
{
printf("x = %d at [%X]/n", x, &x);
printf("y = %d at [%X]/n", y, &y);
printf("z = %d at [%X]/n", z, &z);
}
int main(int argc, char *argv[])
{
foo(100, 200, 300);
return 0;
}
运行结果:
x = 100 at [BFE28760]
y = 200 at [BFE28764]
z = 300 at [BFE28768]
我们知道入栈时,栈顶从高地址向低地址方向增长。而函数调用过程中参数传递是通过栈来实现的,一般调用都是从右至左的顺序压参数入栈。通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。因此,C语言中函数参数的入栈顺序是从右至左正好便利了动态变化参数个数。不管你有多少个参数反正将最右面的那个压入栈底,最左面的参数出入栈顶。参数与参数之间是相邻的,知道前一个参数的类型及地址,根据后一个参数的类型就可以获取后一个参数的内容。对于变长参数函数,结合一定的条件,我们可以根据最后一个指定参数获取之后的省略参数内容。
int func(int a,int b,...);
如,对于函数func,我们知道了参数b的地址及类型,就可知道第一个可变参数的栈地址(如果有的话),如果知道第一个可变参数的类型,就可知道第一个可变参数的内容和第二个可变参数的地址(如果有的话)。以此类推,可以实现对可变参数函数的所有参数的访问。
那么,要怎么指定上诉的“一定的条件”呢?最简单的方法就像printf等函数一样,使用格式化占位符。分析格式化字符串参数,通过事先定义好的格式化占位符可知可变参数的类型及个数,从而获取各个参数内容。一般对于可变参数类型相同的函数也可直接在强制参数中指定可变参数的个数和类型,这样也能获取各个参数的内容。
无论哪种,都涉及对栈地址偏移的操作。结合栈存储模式和系统数据类型的字长,我们可根据可变参数的类型很容易得到栈地址的偏移量。使用va_start、va_arg、va_end三个标准宏来实现栈地址的偏移及获取可变参数内容。这三个宏定义在stdarg.h头文件中,可根据预先定义的系统平台自动获取相应平台上各个数据类型的偏移量。
类型 va_list
保有 va_start 、 va_arg 、 va_end 和 va_copy 所需的信息
宏
va_start 令对可变函数参数的访问可行 void va_start(va_list ap,last);
va_arg 访问下一个可变函数参数 type va_arg(va_list ap,type);
va_copy (C++11)制造可变函数参数的副本 void va_copy(va_list dest,va_list src);
va_end 结束可变参数函数的遍历 void va_end(va_list ap);
概要:调用函数必须声明一个va_list类型的变量,以供宏va_start(),va_arg()和va_end()使用。va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);
对ap 进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;初始化va_list变量,参数last是变长参数表前的最后一个知道类型的变量名;
获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;获取下一个变长参数表里的参数,需要指定类型,以决定返回的对象类型和指针移动的步长;
获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。通常va_start和va_end是成对出现。
//访问可变参数流程
va_listargs;//定义一个可变参数列表
va_start(args,arg);//初始化args指向强制参数arg的下一个参数;
va_arg(args,type);//获取当前参数内容并将args指向下一个参数
...//循环获取所有可变参数内容
va_end(args);//释放args
注意: 对于可变参数函数的调用有一点需要注意,实际的可变参数的个数必须比前面强制参数中指定的个数要多,或者不小于,也即后续参数多一点不要紧,但不能少,如果少了则会访问到函数参数以外的堆栈区域,这样程序可能崩掉。前面强制参数中指定的类型和后面实际参数的类型不匹配也有可能造成程序崩溃。
拥有变长参数的函数在声明定义时其参数个数与类型是不定的,在运行调用时参数的状态则是一定的。而默认参数函数在声明定义时其参数类型与个数都是一定的,只是后面部分参数指定了默认值,可通过省略(不指定)部分参数调用这个默认参数函数。但是默认参数函数还是使用了声明中指定的全部参数,编译器自动给后部分参数赋了默认值;而变长参数函数则仅仅使用了运行调用时提供的参数。