举个例子说明吧
int y=va_arg(x,int);
宏展开成( *(int *)((x += _INTSIZEOF(int)) - _INTSIZEOF(int)) )
此时x指向下一个参数(x = x + _INTSIZEOF(int))
然后x再减去_INTSIZEOF(int)得到x未改变前的地址,
再将x所指向的int类型的值赋给y
也就就是说y取得x所指向的int类型的值,然后x指向下一个参数地址
加了,没减回去。
注意,是:
(ap += _INTSIZEOF(t)) - _INTSIZEOF(t)
而不是:
(ap + _INTSIZEOF(t)) - _INTSIZEOF(t)
ap变了,下一次再取下一个参数时,就不会仍然取到先前的那个了。
写成:
#define va_arg(ap,t) (*(t *)((ap += _INTSIZEOF(t)), ap - _INTSIZEOF(t)) )
更明了
3.#define _crt_va_end(ap) ( ap = (va_list)0 )
将va_list置成无效指针。
以上是printf()变参的实现过程。
实现这样一个函数要在内部使用va_list,va_start,va_arg,va_end,这些都是定义在
stdarg.h中的宏。
va_list是定义了一个保存函数参数的数据结构。
va_start(argp,msg)是将argp指向第一个可变参数,而msg是最后一个确定的参数。
最后一个确定的参数的含义是指它以后的参数都是可变参数,如果有下面的函数声明
void demo(char *msg1,char*msg2,...)
那么这里的最后一个确定参数就是msg2。
va_arg(argp,char *)返回当前参数的值,类型为char*,然后将argp指向下一个变长参
数。从这一步可以看出来我们可以通过va_start和va_arg遍历所有的变长参数。
va_end 将argp的值置为0。
下面我们看看上述几个宏在visual c++.net 2003 中的实现方法。首先是va_list的实现
#ifdef _M_ALPHA
typedef struct {
char*a0;
intoffset;
} va_list;
#else
typedef char * va_list;
#endif
可以看到va_list实际上是一个机器类型相关的宏,除了alpha机器以外,其他机器类
型都被定义为一个char类型的指针变量,之所以定义为char*是因为可以用该变量逐
地址也就是逐字节对参数进行遍历。
从上面可以看到,这些宏的实现都是和机器相关的,下面是大家常用的IX86机器下宏的
相关定义。
#elif defined(_M_IX86)
#define_INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) -1) )
#defineva_start(ap,v) ( ap =(va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#defineva_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#defineva_end(ap) ( ap = (va_list)0 )
#ifdef __cplusplus
#define_ADDRESSOF(v) ( &reinterpret_cast<const char&>(v) )
#else
#define_ADDRESSOF(v) ( &(v) )
#endif
首先看_INTSIZEOF(n)
我们知道对于IX86,sizeof(int)一定是4的整数倍,所以~(sizeof(int) - 1) )的值一定是
右面[sizeof(n)-1]/2位为0,整个这个宏也就是保证了右面[sizeof(n)-1]/2位为0,其余位置
为1,所以_INTSIZEOF(n)的值只有可能是2,4,8,16,......等等,实际上是实现了字节对齐。
#defineva_start(ap,v) ( ap =(va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
所以va_start(ap,v)的作用就很明了了,_ADDRESSOF(v)定义了v的起始地址,_INTSIZEOF(v)定义了v所
占用的内存,所以ap 就指向v后面的参数的起始地址。
#defineva_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
ap+=_INTSIZEOF(t),此时ap指向下一个参数,再减去_INTSIZEOF(t),又获得本来的地址,
将其返回,转化为类型*的指针,再取指针所指地址的值。
ap += _INTSIZEOF(t)使ap指向了后面一个参数的地址
而( *(t *)((ap += _INTSIZEOF(t)) -_INTSIZEOF(t)) )相当于返回了目前t类型的参数的值。
#defineva_end(ap) ( ap = (va_list)0 )
将变量ap 的值置为0。
通过上述分析,再次印证了我么前面对可变参数实现的解释。
因此我们可以总结出变长参数函数的一般实现方法:
1:声明原型,形如void demo(char*msg,...),注意变长参数的原型声明中至少要含有
一个确定参数。
2:用va_list定义保存函数参数的数据结构,可以理解为一个指针变量(稍后会解释)。
3:用va_start将上一步定义的变量指向第一个可变参数。
4:用va_arg遍历所有的可变参数。
5:用va_end将指针变量持有的地址值置为0。
当你的函数的参数个数不确定时,就可以使用上述宏进行动态处理,这无疑为你的程序增加了灵活性。
Example:
◎用法1:
func( Type para1, Type para2, Type para3, ... )
{
/****** Step 1 ******/
va_list ap;
va_start( ap, para3 ); //一定要“...”之前的那个参数
/****** Step 2 ******/
//此时ap指向第一个可变参数
//调用va_arg取得里面的值
Type xx = va_arg( ap, Type );
//Type一定要相同,如:
//char *p = va_arg( ap, char *);
//int i = va_arg( ap, int );
//如果有多个参数继续调用va_arg
/****** Step 3 ******/
va_end(ap); //For robust!
}
◎用法2:
CString AppendString(CString str1,...)//一个连接字符串的函数,参数个数可以动态变化
{
LPCTSTR str=str1;//str需为指针类型,因为va_arg宏返回的是你的参数的指针,但是如果你的参数为int等简 //单类型,则不必为指针,因为变量名实际上即是指针。
CString res;
va_list marker; //你的类型链表
va_start(marker,str1);//初始化你的marker链表
while(str!="ListEnd")//ListEnd:参数的结束标志,十分重要,在实际中需自行指定
{
res+=str;
str=va_arg(marker,CString);//取得下一个指针
}
va_end(marker);//结束,与va_start合用
return res;
}
int main()
{
CString str=AppendString("xu","zhi","hong","ListEnd");
cout<<str.GetBuffer(str.GetLength())<<endl;
return 0;
}
输出 xuzhihong
CString AppendString(CString str1,...),因为连接字符串的参数可以动态变化,你不知用户要进行连接的字符串个数是多少,所以你可以用…来代替。但是要注意的是你的函数要有一个参数作为标志来表示结束,否则会出错。在上例中用ListEnd作为结束符。还有va_arg返回的是你参数内容的指针。上例在支持MFC程序的console下运行通过。
可变参数函数的原型声明格式为:
type VAFunction(type arg1, type arg2, … );
参数可以分为两部分:个数确定的固定参数和个数可变的可选参数。函数至少需要一个固定参数,固定参数的声明和普通函数一样;可选参数由于个数不确定,声明时用"…"表示。固定参数和可选参数公同构成一个函数的参数列表。
借助上面这个简单的例2,来看看各个va_xxx的作用。
va_list arg_ptr:定义一个指向个数可变的参数列表指针;
va_start(arg_ptr, argN):使参数列表指针arg_ptr指向函数参数列表中的第一个可选参数,说明:argN是位于第一个可选参数之前的固定参数,(或者说,最后一个 固定参数;…之前的一个参数),函数参数列表中参数在内存中的顺序与函数声明时的顺序是一致的。如果有一va函数的声明是void va_test(char a, char b, char c, …),则它的固定参数依次是a,b,c,最后一个固定参数argN为c,因此就是va_start(arg_ptr, c)。
va_arg(arg_ptr, type):返回参数列表中指针arg_ptr所指的参数,返回类型为type,并使指针arg_ptr指向参数列表中下一个参数。
va_copy(dest, src):dest,src的类型都是va_list,va_copy()用于复制参数列表指针,将dest初始化为src。
va_end(arg_ptr):清空参数列表,并置参数指针arg_ptr无效。说明:指针arg_ptr被置无效后,可以通过调用va_start ()、va_copy()恢复arg_ptr。每次调用va_start() / va_copy()后,必须得有相应的va_end()与之匹配。参数指针可以在参数列表中随意地来回移动,但必须在va_start() … va_end()之内。
va函数的实现就是对参数指针的使用和控制。
|
|
|
从上图中可以很清楚地看出va_xxx宏如此编写的原因。
1. va_start。为了得到第一个可选参数的地址,我们有三种办法可以做到:
A) = &n3 + _INTSIZEOF(n3)
// 最后一个固定参数的地址 + 该参数占用内存的大小
B) = &n2 + _INTSIZEOF(n3) + _INTSIZEOF(n2)
// 中间某个固定参数的地址 + 该参数之后所有固定参数占用的内存大小之和
C) = &n1 + _INTSIZEOF(n3) + _INTSIZEOF(n2) + _INTSIZEOF(n1)
// 第一个固定参数的地址 + 所有固定参数占用的内存大小之和
从编译器实现角度来看,方法B),方法C)为了求出地址,编译器还需知道有多少个固定参数,以及它们的大小,没有把问题分解到最简单,所以不是很聪明的途 径,不予采纳;相对来说,方法A)中运算的两个值则完全可以确定。va_start()正是采用A)方法,接受最后一个固定参数。调用va_start ()的结果总是使指针指向下一个参数的地址,并把它作为第一个可选参数。在含多个固定参数的函数中,调用va_start()时,如果不是用最后一个固定 参数,对于编译器来说,可选参数的个数已经增加,将给程序带来一些意想不到的错误。(当然如果你认为自己对指针已经知根知底,游刃有余,那么,怎么用就随 你,你甚至可以用它完成一些很优秀(高效)的代码,但是,这样会大大降低代码的可读性。)
注意:宏va_start是对参数的地址进行操作的,要求参数地址必须是有效的。一些地址无效的类型不能当作固定参数类型。比如:寄存器类型,它的地址不是有效的内存地址值;数组和函数也不允许,他们的长度是个问题。因此,这些类型时不能作为va函数的参数的。
2. va_arg身兼二职:返回当前参数,并使参数指针指向下一个参数。
初看va_arg宏定义很别扭,如果把它拆成两个语句,可以很清楚地看出它完成的两个职责。
|
通过固定参数部分必须能判断出其后的参数的个数与类型.
如:约定可变参数类型,第一个参数说明可变参数的个数.
通过解析格式串得到可变参数的个数与类型.