可变参数函数是指函数参数的个数、类型等是不固定的,需要在用户调用过程中,根据实际传入的参数来确定其类型、个数等信息。例如:可变参数函数printf可谓是在C开发过程中使用最多的标准输出库函数之一,因此有必要对其原理进行了解以便更好使用之,同时在一些开发过程中可能还需要自己来实现一个可变参数的函数。本文主要是关于C中可变参数函数的一些总结,包括可变参数的实现原理及其实现方式。
可变参数函数的原理:
1、如果要自定义一个可变参数的函数,就必须包含头文件stdarg.h,例如:#include <stdarg.h>。该文件中主要做了哪些事情呢?
其实主要是在该头文件中定义了几个跟可变参数函数实现相关的重要的宏:va_arg、va_end、va_start、va_list。这些宏都以va_开头,va即variable argument,且都可重入。这些函数的定义及其作用如下所示:
2、各宏的定义:
va_list 定义形式为:
typedef char * va_list;
可以看到它实质上是一个字符指针,主要用于将该类型指针变量指向可变参数列表的首地址。
va_arg、va_end、va_start 定义比较绕人:首先在stdarg.h里面进行了如下定义:
#define va_start _crt_va_start
#define va_arg _crt_va_arg
#define va_end _crt_va_end,
继续寻找_crt_va_start等的定义可以发现是如下的形式:
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap) ( ap = (va_list)0 )
再跟进去,可以发现 _ADDRESSOF的定义:
#ifdef __cplusplus
#define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) )
#else
#define _ADDRESSOF(v) ( &(v) )
#endif
可以理解为_ADDRESSOF(v)就是取参数v的地址。
以及 _INTSIZEOF的定义:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
这句话的意思就是按照字节对齐之后的数据长度。
具体可参考:http://houjixin.blog.163.com/blog/static/35628410201321943745214/
或者:http://blog.csdn.net/hjx_1000/article/details/8692420
简单起见,可以认为是这样定义的:
#define va_start(ap,v) ( ap = (va_list)&v + sizeof(v) )
#define va_arg(ap,t) ( ((t *)ap)++[0] )
#define va_end(ap) ( ap = (va_list)0 )
3、各宏的说明
va_arg:
va_arg(argptr/*可选的参数列表*/,type/*下一个参数的类型*/);
说明:宏va_arg用于从一个可变长度参数列表索引argptr提取并列参数,type参数指定提取参数的类型。本宏对每个参数只能调用一次,且必须根据参数列表中的参数顺序调用。第一次调用宏va_arg则返回va_start宏中指定的prevparm参数后的第一个参数。后来对宏va_arg的调用依次返回余下的参数。
返回值:宏va_arg返回指定参数类型的值
va_end
va_end(argptr/*可选的参数列表*/);
说明:宏va_end用于种植可变长度参数列表指针argptr的使用,其中参数argptr是由宏va_start初始化的,终止之后可以防止误引用。
va_start
va_start(argptr/*可选的参数列表*/,prevparp/*可选参数前的参数*/)
说明:宏va_start用在一个可变长度参数列表的函数中时,用va_arg和va_end宏初始化和终止参数argptr。Prevparp参数必须是用省略号(…)指定的可选参数前紧挨的函数参数。需要在使用va_arg之前初始化可变长度参数列表指针。
下面是改写别人的例子:
void Test(int iNum, ...)
{
int Param1;
//定义一个指针,用于指向栈上可变参数(即省略号...)的首地址,
va_list ap;
//将ap指向iNum之后的地址,也就是指向栈上可变参数(即省略号...)的首地址
va_start(ap, iNum);
//取可变参数列表中第一个数值(其类型是整形),注意这里必须传入的也是整形,如果传入的和取的不一样就会造成访问错误
Param1 = va_arg(ap,int);
//终止指针ap
va_end(ap);
//将从可变参数列表中取出的数值显示出来
printf("iNum=%d, Param1%d",iNum, Param1);
}
可变长度参数也是需要已知可变参数的类型个数之后才能进行正确的读取解析的,只不过这个知道的时间要等到调用时才能知道,再看下面一个改写的例子:
#include <stdarg.h>
#include <stdio.h>
int varFunc(int iType, ...)
{//这个可变参数的函数只能解析两种类型且长度为3的可变参数,其他的都无法解析
va_list ap;
va_start(ap, iType);
if (0 == iType)
{//只能解析第一个参数为int,第二参数为char*,第三个参数为long的可变参数,其他类型和个数的都不一定能正确解析
int arg1;
char* arg2;
long arg3;
arg1 = va_arg(ap,int);
arg2 = va_arg(ap,char*);
arg3 = va_arg(ap,long);
}
else
{//只能解析第一个参数为char*,第二参数为char*,第三个参数为long的可变参数,其他类型和个数的都不一定能正确解析
char* arg1;
char* arg2;
long arg3;
arg1 = va_arg(ap, char*);
arg2 = va_arg(ap, char*);
arg3 = va_arg(ap, long);
}
}
int main()
{
varFunc(0, 27, "test01", 110L);
varFunc(1, "ssss", "test01", 110L);
return 0;
}
4、另一个例子
变长参数的实现是基于C语言默认的cdecl调用惯例的自左至右的压栈方式,设想如下的函数:(参考自《程序员的自我修养——连接、装载和库》
int sum(unsignediNum,...);
该函数的意思是第一个参数传递一个整数iNum,后面紧挨着iNum个整数,然后该函数返回iNum个整数的和,当我们做如下调用时:
int n = sum(3,16,38,53);
这些参数在栈上形成的布局如下图所示:
在该函数内,可以使用iNum来访问参数3,但是没有办法使用名字来访问其他几个不定的参数,但是这些可变参数在栈上是紧挨着其前面的确定名称的参数iNum存放的,因此,我们可以通过iNum的地址来计算出这些可变参数的地址,上述sum函数的实现可理解成下面这个样子:
int sum(unsigned iNum,...)
{
int* p = NULL;//这里就相当于前面所述的定义:va_list p
p = &iNum + 1;//这里相当于调用了va_start(ap, iNum);
int iRes = 0;
while(iNum--)
{
iRes += *p++;//这里每次取一个参数,相当于前面所述的va_arg(ap,int);
}
//当然这里结束没有执行p= NULL,即没有对应的va_end(ap);这是因为这是个函数的局部变量,我们知道后面肯定不会再有地方引用它了,因此不用再让指针赋值为空
return iRes;
}
5、一个自己实现的printf函数,相信通过阅读上面的部分之后,读懂下面这个函数应该不成问题:
bool myprintf(char* fmt/*IN*/,...)
{
if(NULL == fmt)
return false;
int iParam;
char* pCurHandlePos = NULL;
char savedBuf[MAX_LOGITEM_LEN];
memset(savedBuf,0,MAX_LOGITEM_LEN);
char* pCurSavePos = savedBuf;
va_list vaParamList;//定义一个用于指向可变参数列表(即函数形参中的...)地址变量
va_start(vaParamList, fmt);//获取可变参数列表的地址(紧挨着定参数fmt之后的地址),并将其放入vaParamList
for (pCurHandlePos= fmt; *pCurHandlePos&&(pCurSavePos-savedBuf<MAX_LOGITEM_LEN); pCurHandlePos++)
{
if (*pCurHandlePos != '%')
{
*pCurSavePos++= *pCurHandlePos;
continue;
}
switch (*++pCurHandlePos)
{
case 'd':
iParam= va_arg(vaParamList, int);
snprintf(pCurSavePos,MAX_LOGITEM_LEN,"%d",iParam);
pCurSavePos = savedBuf + strlen(savedBuf);
break;
case 'o':
iParam= va_arg(vaParamList, int);
snprintf(pCurSavePos,MAX_LOGITEM_LEN,"0%o",iParam);
pCurSavePos = savedBuf + strlen(savedBuf);
break;
case 'x':
iParam= va_arg(vaParamList, int);
snprintf(pCurSavePos,MAX_LOGITEM_LEN,"0x%x",iParam);
pCurSavePos = savedBuf + strlen(savedBuf);
break;
case 'c':
char cParam;
cParam= va_arg(vaParamList, int);
*pCurSavePos++= cParam;
break;
case 'f':
double dParam;
dParam= va_arg(vaParamList, double);
snprintf(pCurSavePos,MAX_LOGITEM_LEN,"%f",dParam);
pCurSavePos = savedBuf + strlen(savedBuf);
break;
case 's':
char* pSParam = NULL;
for (pSParam = va_arg(vaParamList, char *) ; *pSParam ; pSParam++ )
*pCurSavePos++= *pSParam;
break;
}
}
*pCurSavePos= '\0';
printf(savedBuf);
va_end (vaParamList); //将参数列表指针置空,防止后续再次引用出现误操作
}