一、在C中,当我们无法列出传递函数的所有实参的类型和数目时,可以用省略号指定参数表。
void fun(...);
void fun(parm_list,...);
#include <stdio.h> void fun(int n,...) { int *temp=&n; temp++; for(int i=0;i<n;i++) { printf("%d ",*temp); temp++; } } int main() { int a=1,b=2,c=3,d=4; fun(4,a,b,c,d); return 0; }
二、使用va_start,va_arg,va_end
#include <stdio.h> #include <stdarg.h> void fun(int n,...) //变参函数,C++里也有 { int temp,i; va_list ap; //step1:va_list类型数据可以保存函数的所有参数,做为一个列表一样保存 va_start(ap,n); //step2:对ap 进行初始化,让ap指向可变参数表里面的第一个参数(最后一个固定参数后的可变参数) for(i=0;i<n;i++) { temp=va_arg(ap,int); //step3:把 ap 的位置指向变参表的下一个变量位置 printf("%d ",temp); } va_end(ap); //step4:关闭ap指针 } int main() { int a=1,b=2,c=3,d=4; fun(4,a,b,c,d); return 0; }
<Step 1> 在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);
<Step 2> 然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
<Step 3> 然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
<Step 4> 获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。说白了,就是让我们的程序具有健壮性。通常va_start和va_end是成对出现。
三、函数参数传递原理
这里要知道两个事情:
⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
(2)在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
(2)在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
栈底 高地址 | ....... | 函数返回地址 | ....... | 函数最后一个参数 | .... | 函数第一个可变参数 <--va_start后ap指向 | 函数最后一个固定参数 | 函数第一个固定参数 栈顶 低地址
所使用到的宏:
void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr );
宏解释:
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_start(ap, v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。
4、va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
因此,现在再来看va_arg()的实现就应该心中有数了:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏做了两个事情,
①ap指向下一个参数:ap += _INTSIZEOF(t)
因此,现在再来看va_arg()的实现就应该心中有数了:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏做了两个事情,
①ap指向下一个参数:ap += _INTSIZEOF(t)
②表达式取得本参数的值:先是让ap指向下一个参数:ap +=
_INTSIZEOF(t),然后在减去_INTSIZEOF(t),使得表达式结果为ap之前的值,即当前需要得到的参数的地址,强制转换成指向此参数的
类型的指针,然后用*取值
int main(void) { int a=0,b=0; b=((a+=1)-1); printf("a:%d,b:%d ",a,b); b=((a+=1)-1); printf("a:%d,b:%d",a,b); return 0; }
5、va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.