原理说明:
VA_LIST
用c语言解决变参问题的一组宏,位于头文件下。
VA_LIST的使用方法: (1)首先在函数中定义VA_LIST型的变量。 该变量是指向参数的指针
)2)然后,用VA_START宏初始化刚定义变量的VA_LIST变量。 此宏的第二个参数是第一个可变参数之前的参数,它是固定参数。
)3)然后在VA_ARG中返回可变的参数。 VA_ARG的第二个参数是要返回的参数类型。
)4)最后在VA_END宏中结束可变参数的获取。 您可以在函数中使用第二个参数。 如果函数具有多个可变参数,则依次调用VA_ARG以获取每个参数。
编译器处理VA_LIST :
)1)执行va_start ) ap,v )之后,ap指向在栈中的第一个可变参数的地址。
)2) VA_ARG )获取类型t的可变参数值,在该步骤中,首先apt=
sizeof(t型)使ap指向下一个参数的地址。 然后返回堆栈中第一个变量的地址AP-sizeof(t型)指针。 然后,用*获取这个地址的内容。
)3) VA_END ),将X86平台定义为ap=
(char* )0)直接定义为ap不再指向堆栈(与NULL一样,部分为) void* )0),以防止编译器生成VA_END的代码。 例如,gcc是在Linux的X86平台上这样定义的。
请注意,参数不能声明为寄存器变量,也不能声明为函数或数组类型,因为参数的地址用于VA_START宏。
使用VA_LIST应注意的问题:
)1)由于va_start,va_arg,
因为va_end等被定义为宏,所以它看起来很愚蠢。 可变参数的类型和个数完全在其函数内由程序代码控制,无法智能识别不同参数的个数和类型。
也就是说,如果你想实现智能识别可变参数,就通过自己的程序判断来实现。
)2)另一个问题是编译器没有充分检查可变参数函数的原型,不利于编程的检查错误。不利于编写高质量的代码。
总结:可变参数的函数原理其实很简单,但VA系列是在宏定义中定义的,实现与堆栈相关。 我们写可变函数的c函数时,有利也有弊,所以在不必要的场合不需要使用可变参数。 如果在C,应该利用C多态性实现变参数功能,尽量避免用C语言实现。
va_list ap;
//参数列表声明用于转换va_start(AP,fmt )的变量;
//初始化变量va_end(AP; //结束变量列表,与va_start配对时,可以从va_arg(AP,type )中取出参数
调试成功的输出程序
#包含
#包含
#define bufsize 80
char buffer[bufsize];
intVSPF(char*fmt,) ) ) )。
{
va_list argptr;
int cnt;
va_start(argptr,fmt );
CNT=vsnprintf(buffer,bufsize,fmt,argptr );
va_end(argptr );
return(CNT;
}
入主(void ) )。
{
int inumber=30;
float fnumber=90.0;
char string[4]='abc ';
VPF(%d%f%s )、inumber、fnumber和string );
printf(%s(n ),buffer );
返回0;
}
vsnprintf : int vsnprintf (char * str,size_t size,const char
*format,va_list ap;
write output to character sting str
return value : thenumberofcharacters
打印(notincludingthetrailing '0' usedtoendoutputto
strings(.thefunctionssnprintf ) (and vsnprintf ) ) do not write
morethansizebytes (includingthetrailing '0' ).If the output
wastruncatedduetothislimitthenthereturnvalueisthenumber
ofcharacters(not
including the trailing ' ') which would havebeen written to the final string if enough space had been
available. Thus, a return value of size or more means that the
output was truncated. If an output error is encountered, a
negative
value is returned.
if (return_value >
-1) size = n+1;
else size *= 2;
The glibc implementation of the functions snprintf() and
vsnprintf() conforms to the C99 standard, i.e., behaves as
described above, since glibc
version 2.1. Until glibc 2.0.6 they would return -1 when the out
put was truncated.
C语言用va_start等宏来处理这些可变参数。这些宏看起来很复杂,其实原理挺简单,就是根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。下面我们来分析这些宏。 在stdarg.h头文件中,针对不同平台有不同的宏定义,我们选取X86平台下的宏定义:
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 )
_INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。
为了能从固定参数依次得到每个可变参数,va_start,va_arg充分利用下面两点:
1. C语言在函数调用时,先将最后一个参数压入栈
2. X86平台下的内存分配顺序是从高地址内存到低地址内存
高位地址
第N个可变参数
。。。
第二个可变参数
第一个可变参数 ? ap
固定参数 ? v
低位地址
由上图可见,v是固定参数在内存中的地址,在调用va_start后,ap指向第一个可变参数。这个宏的作用就是在v的内存地址上增加v所占的内存大小,这样就得到了第一个可变参数的地址。
接下来,可以这样设想,如果我能确定这个可变参数的类型,那么我就知道了它占用了多少内存,依葫芦画瓢,我就能得到下一个可变参数的地址。
让我再来看看va_arg,它先ap指向下一个可变参数,然后减去当前可变参数的大小即得到当前可变参数的内存地址,再做个类型转换,返回它的值。
要确定每个可变参数的类型,有两种做法,要么都是默认的类型,要么就在固定参数中包含足够的信息让程序可以确定每个可变参数的类型。比如,printf,程序通过分析format字符串就可以确定每个可变参数大类型。
最后一个宏就简单了,va_end使得ap不再指向有效的内存地址。
其实在varargs.h头文件中定义了UNIX System
V实行的va系列宏,而上面在stdarg.h头文件中定义的是ANSI C形式的宏,这两种宏是不兼容的,一般说来,我们应该使用ANSI
C形式的va宏。
定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.C语言的函数是从右向左压入堆栈的,函数的参数在堆栈中的分布位置.我
们看到va_list被定义成char*,有一些平台或操作系统定义为void*.再看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的
地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址:
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n个参数(第一个可变参数) |
|-----------------------------|
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|
&v
然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我们看一下va_arg取int型的返回值:
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回
ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址
然后用*取得这个地址的内容(参数值)赋给j.
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n个参数(第一个可变参数) |
|-----------------------------|
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|
&v
最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不
会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.关于va_start,
va_arg, va_end的描述就是这些了,我们要注意的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.
System V Unix把va_start定义为只有一个参数的宏:
va_start(va_list arg_ptr);
而ANSI C则定义为:
va_start(va_list arg_ptr, prev_param);
如果我们要用system V的定义,应该用vararg.h头文件中所定义的
宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以
用ANSI C的定义就够了,也便于程序的移植.
可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数.如果在C++里,我们应该利用C++的多态性来实现可变参数的功能,尽量避免用C语言的方式来实现