分类: 其他平台
2015-03-18 11:19:39
可变参数函数的原型声明格式为:
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()之内。
ANSI C标准下,va的宏定义在stdarg.h中,它们有:va_list,va_start(),va_arg(),va_end()。
简单地说,va函数的实现就是对参数指针的使用和控制。
typedef char * va_list; // x86平台下va_list的定义
函数的固定参数部分,可以直接从函数定义时的参数名获得;对于可选参数部分,先将指针指向第一个可选参数,然后依次后移指针,根据与结束标志的比较来判断是否已经获得全部参数。因此,va函数中结束标志必须事先约定好,否则,指针会指向无效的内存地址,导致出错。
这里,移动指针使其指向下一个参数,那么移动指针时的偏移量是多少呢,没有具体答案,因为这里涉及到内存对齐(alignment)问题,内存对齐跟具体使用的硬件平台有密切关系,比如大家熟知的32位x86平台规定所有的变量地址必须是4的倍数(sizeof(int) = 4)。va机制中用宏_INTSIZEOF(n)来解决这个问题,没有这些宏,va的可移植性无从谈起。
首先介绍宏_INTSIZEOF(n),它求出变量占用内存空间的大小,是va的实现的基础。
#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 ) // 将指针置为无效
下表是针对函数int TestFunc(int n1, int n2, int n3, …) 参数传递时的内存堆栈情况。(C编译器默认的参数传递方式是__cdecl。)
对该函数的调用为int result = TestFunc(a, b, c, d. e); 其中e为结束标志。
在使用va-arg时,要注意第二个参数所用类型名应与传递到堆栈的参数的字节数对应,以保证能对不同类型的可变参数进行正确地寻址,比如实参依次为char型、char * 型、int型和float型时,在va-arg中它们的类型则应分别为int、char *、int和double.
void va-end(va-list ap)也是一个宏,该宏用于被调用函数完成正常返回,功能就是把指针ap赋值为0,使它不指向内存的变量。下面给出va_end在C中的源码:
#define va_end(ap) ( ap = (va_list)0 )
va-end必须在va-arg读完所有参数后再调用,否则会产生意想不到的后果。特别地,当可变参数表函数在程序执行过程中不止一次被调用时,在函数体每次处理完可变参数表之后必须调用一次va-end,以保证正确地恢复栈。
一个变参数函数至少需要有一个普通参数,其普通参数可以具有任何类型。在函数定义中,这种函数的最后一个普通参数除了一般的用途之外,还有其他特殊用途。下面从一个例子开始说明有关的问题。
假设我们想定义一个函数sum,它可以用任意多个整数类型的表达式作为参数进行调用,希望sum能求出这些参数的和。这时我们应该将sum定义为一个只有一个普通参数,并具有变长度参数表的函数,这个函数的头部应该是(函数原型与此类似):
int sum(int n, ...)
我们实际上要求在函数调用时,从第一个参数n得到被求和的表达式个数,从其余参数得到被求和的表达式。在参数表最后连续写三个圆点符号,说明这个函数具有可变数目的参数。凡参数表具有这种形式(最后写三个圆点),就表示定义的是一个变参数函数。注意,这样的三个圆点只能放在参数表最后,在所有普通参数之后。
下面假设函数sum里所用的va_list类型的变量的名字是vap。在能够用vap访问实际参数之前,必须首先用宏a_start对这个变量进行初始化。宏va_start的类型特征可以大致描述为:
va_start(va_list vap, 最后一个普通参数)
在函数sum里对vap初始化的语句应当写为:
va_start(vap, n); 相当于 char *vap= (char *)&n + sizeof(int);
此时vap正好指向n后面的可变参数表中的第一个参数。
我们还需要注意一个陷阱,即va_arg宏的第2个参数不能被指定为char、short或者float类型。在可变参数函数传递时,因为char和short类型的参数会被提升为int类型,而float类型的参数会被提升为double类型 。
例如,以下的代码是错误的
因为我们无法传递一个char类型参数,如果传递了,它将会被自动转化为int类型。上面的式子应该写成:
1 //第一个参数定义可变参数个数,用于循环获取变参内容
2 void ParseVarArgByNum(int dwArgNum, ...){
3 va_list pArgs = NULL;
4 va_start(pArgs, dwArgNum);
5 int dwArgIdx;
6 int dwArgVal = 0;
7 for(dwArgIdx = 1; dwArgIdx <= dwArgNum; dwArgIdx++){
8 dwArgVal = va_arg(pArgs, int);
9 printf("The %dth Argument: %d\n",dwArgIdx, dwArgVal);
10 }
11 va_end(pArgs);
12 }
函数定义一个结束标记,调用时通过最后一个参数传递该标记,以结束变参的遍历打印
//最后一个参数作为变参结束符(-1),用于循环获取变参内容
void ParseVarArgByEnd(int dwStart, ...){
va_list pArgs = NULL;
va_start(pArgs, dwStart);
int dwArgIdx = 0;
int dwArgVal = dwStart;
while(dwArgVal != -1){
++dwArgIdx;
printf("The %dth Argument: %d\n",dwArgIdx, dwArgVal);
dwArgVal = va_arg(pArgs, int); //得到下个变参值
}
va_end(pArgs);
} 函数自定义一些可能出现的参数类型,在变参列表中显式指定变参类型。可这样传递参数:参数数目,可变参数类型1,可变参数值1,可变参数类型2,可变参数值2,
//可变参数采用的形式传递,以处理不同的变参类型 typedef enum{ CHAR_TYPE = 1, INT_TYPE, LONG_TYPE, FLOAT_TYPE, DOUBLE_TYPE, STR_TYPE }E_VAR_TYPE; void ParseVarArgType(int dwArgNum, ...){ va_list pArgs = NULL; va_start(pArgs, dwArgNum); int i = 0; for(i = 0; i < dwArgNum; i++){ E_VAR_TYPE eArgType = va_arg(pArgs, int); switch(eArgType){ case INT_TYPE: printf("The %dth Argument: %d\n", i+1, va_arg(pArgs, int)); break; case STR_TYPE: printf("The %dth Argument: %s\n", i+1, va_arg(pArgs, char*)); break; default: break; } } va_end(pArgs); } 实现简易的MyPrintf函数。该函数无返回值,即不记录输出的字符数目;接受"%d"按整数输出、"%c"按字符输出、"%b"按二进制输出,"%%"输出'%'本身
char *MyItoa(int iValue, char *pszResBuf, unsigned int uiRadix){ //If pszResBuf is NULL, string "Nil" is returned. if(NULL == pszResBuf){ //May add more trace/log output here return "Nil"; } //If uiRadix(Base of Number) is out of range[2,36], //empty resulting string is returned. if((uiRadix < 2) || (uiRadix > 36)){ //May add more trace/log output here *pszResBuf = '\0'; return pszResBuf; } char *pStr = pszResBuf; //Pointer to traverse string char *pFirstDig = pszResBuf; //Pointer to first digit if((10 == uiRadix) && (iValue < 0)){ //Negative decimal number iValue = (unsigned int)-iValue; *pStr++ = '-'; pFirstDig++; //Skip negative sign } int iTmpValue = 0; do{ iTmpValue = iValue; iValue /= uiRadix; //Calculating the modulus operator(%) by hand saving a division *pStr++ = "0123456789abcdefghijklmnopqrstuvwxyz"[iTmpValue - iValue * uiRadix]; }while(iValue); *pStr-- = '\0'; //Terminate string, pStr points to last digit(or negative sign) //Now have a string of number in reverse order //Swap *pStr and *pFirstDig for reversing the string of number while(pFirstDig < pStr){ //Repeat until halfway char cTmpChar = *pStr; *pStr--= *pFirstDig; *pFirstDig++ = cTmpChar; } return pszResBuf; } void MyPrintf(const char *pszFmt, ... ){ va_list pArgs = NULL; va_start(pArgs, pszFmt); for(; *pszFmt != '\0'; ++pszFmt){ //若不是控制字符则原样输出字符 if(*pszFmt != '%'){ putchar(*pszFmt); continue; } //若是控制字符则查看下一字符 switch(*++pszFmt){ case '%': //连续两个'%'输出单个'%' putchar('%'); break; case 'd': //按照整型输出 printf("%d", va_arg(pArgs, int)); break; case 'c': //按照字符输出 printf("%c", va_arg(pArgs, int)); //不可写为...va_arg(pArgs, char); break; case 'b': {//按照二进制输出 char aucStr[sizeof(int)*8 + 1] = {0}; fputs(MyItoa(va_arg(pArgs, int), aucStr, 2), stdout); //printf(MyItoa(va_arg(pArgs, int), aucStr, 2)); break; } default: vprintf(--pszFmt, pArgs); return; } }//end of for-loop va_end(pArgs); }
问题1:可变长参数的获取
有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:
va_arg (argp, float);
这样做可以吗?
答案与分析:
不可以。在可变长参数中,应用的是"加宽"原则。也就是float类型被扩展成double;char、 short类型被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对char和short类型的则用va_arg(argp, int)。
问题2:定义可变长参数的一个限制
为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数?
- int f(...)
- {
- ......
- ......
- ......
- }
答案与分析: 不可以。这是ANSI C 所要求的,你至少得定义一个固定参数。这个参数将被传递给va_start(),然后用va_arg()和va_end()来确定所有实际调用时可变长参数的类型和值。 问题3:如何判别可变参数函数的参数类型? 函数形式如下:
- void fun(char *str ,...)
- {
- ......
- ......
- ......
- }
若传的参数个数大于1,如何判别第2个以后传参的参数类型??? 答案与分析: 这个是没有办法判断的,例如printf( "%d%c%s ", ....)是通过格式串中的%d、 %c、 %s来确定后面参数的类型,其实你也可以参考这种方法来判断不定参数的类型。