可变参数函数详解
可变参数函数又称参数个数可变函数(本文也简称变参函数),即函数参数数目可变。原型声明格式为:
type VarArgFunc(type FixedArg1, type FixedArg2, …); |
其中,参数可分为两部分:数目确定的固定参数和数目可变的可选参数。函数至少需要一个固定参数,其声明与普通函数参数相同;可选参数由于数目不定(0个或以上),声明时用"…"表示(“…”用作参数占位符)。固定参数和可选参数共同构成可变参数函数的参数列表。
由于参数数目不定,使用可变参数函数通常能缩短编码,灵活性和易用性较高。
典型的变参函数如printf(及其家族),其函数原型为:
int printf(const char* format, ...); |
printf函数除参数format固定外,后续参数的数目和类型均可变。实际调用时可有以下形式:
printf("string"); printf("%d", i); printf("%s", s); printf("number is %d, string is:%s", i, s); …… |
1 变参函数实现原理
C调用约定下可使用va_list系列变参宏实现变参函数,此处va意为variable-argument(可变参数)。典型用法如下:
#include <stdarg.h> int VarArgFunc(int dwFixedArg, ...){ //以固定参数的地址为起点依次确定各变参的内存起始地址 va_list pArgs = NULL; //定义va_list类型的指针pArgs,用于存储参数地址 va_start(pArgs, dwFixedArg); //初始化pArgs指针,使其指向第一个可变参数。该宏第二个参数是变参列表的前一个参数,即最后一个固定参数 int dwVarArg = va_arg(pArgs, int); //该宏返回变参列表中的当前变参值并使pArgs指向列表中的下个变参。该宏第二个参数是要返回的当前变参类型 //若函数有多个可变参数,则依次调用va_arg宏获取各个变参 va_end(pArgs); //将指针pArgs置为无效,结束变参的获取 /* Code Block using variable arguments */ } //可在头文件中声明函数为extern int VarArgFunc(int dwFixedArg, ...);,调用时用VarArgFunc(FixedArg, VarArg); |
变参宏根据堆栈生长方向和参数入栈特点,从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。
变参宏的定义和实现因操作系统、硬件平台及编译器而异(但原理相似)。System V Unix在varargs.h头文件中定义va_start宏为va_start(va_list arg_ptr),而ANSI C则在stdarg.h头文件中定义va_start宏为va_start(va_list arg_ptr, prev_param)。两种宏并不兼容,为便于程序移植通常采用ANSI C定义。
gcc编译器使用内置宏间接实现变参宏,如#define va_start(v,l) __builtin_va_start(v,l)。因为gcc编译器需要考虑跨平台处理,而其实现因平台而异。例如x86-64或PowerPC处理器下,参数不全都通过堆栈传递,变参宏的实现相比x86处理器更为复杂。
x86平台VC6.0编译器中,stdarg.h头文件内变参宏定义如下:
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, type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) ) #define va_end(ap) ( ap = (va_list)0 ) |
各宏的含义如下:
①_INTSIZEOF宏考虑到某些系统需要内存地址对齐。从宏名看应按照sizeof(int)即堆栈粒度对齐,即参数在内存中的地址均为sizeof(int)=4的倍数。例如,若在1≤sizeof(n)≤4,则_INTSIZEOF(n)=4;若5≤sizeof(n)≤8,则_INTSIZEOF(n)=8。
为便于理解,简化该宏为
#define _INTSIZEOF(n) ((sizeof(n) + x) & ~(x)) x = sizeof(int) - 1 = 3 = 0b’0000 0000 0000 0011 ~x = 0b’1111 1111 1111 1100 |
一个数与(~x)相与的结果是sizeof(int)的倍数,即_INTSIZEOF(n)将n圆整为sizeof(int)的倍数。
②va_start宏根据(va_list)&v得到第一个可变参数前的一个固定参数在堆栈中的内存地址,加上_INTSIZEOF(v)即v所占内存大小后,使ap指向固定参数后下个参数(第一个可变参数地址)。
固定参数的地址用于va_start宏,因此不能声明为寄存器变量(地址无效)或作为数组类型(长度难定)。
③va_arg宏取得type类型的可变参数值。首先ap+=_INTSIZEOF(type),即ap跳过当前可变参数而指向下个变参的地址;然后ap-_INTSIZEOF(type)得到当前变参的内存地址,类型转换后返回当前变参值。
va_arg宏的等效实现如下
//将指针移动至下个变参,并返回左移的值[-1](数组下标表示偏移量),即当前变参值 #define va_arg(ap,type) ((type *)((ap) += _INTSIZEOF(type)))[-1] |
④va_end宏使ap不再指向有效的内存地址。该宏的某些实现定义为((void*)0),编译时不会为其产生代码,调用与否并无区别。但某些实现中va_end宏用于函数返回前完成一些必要的清理工作:如va_start宏可能以某种方式修改堆栈,导致返回操作无法完成,va_end宏可将有关修改复原;又如va_start宏可能对参数列表动态分配内存以便于遍历va_list,va_end宏可释放此前动态分配的内存。因此,从使用va_start宏的函数中退出之前,必须调用一次va_end宏。
函数内可多次遍历可变参数,但每次必须以va_start宏开始,因为遍历后ap指针不再指向首个变参。
下图给出基于变参宏的可变参数在堆栈中的分布:
变参宏无法智能识别可变参数的数目和类型,因此实现变参函数时需自行判断可变参数的数目和类型。前者可显式提供变参数目或设定遍历结束条件(如-1、' '或回车符等)。后者可显式提供变参类型枚举值,或在固定参数中包含足够的类型信息(如printf函数通过分析format字符串即可确定各变参类型),甚至主调函数和被调函数可约定变参的类型组织等。
2 变参函数代码示例
本节给出若干遵循ANSI C标准形式的简单可变参数函数,基于这些示例可构造更为复杂实用的功能。
示例函数必须包含stdio.h和stdarg.h头文件,并按需包含string.h头文件。
【示例1】函数接受一个整型固定参数和一个整型可变参数,并打印这两个参数值。
1 void IntegerVarArgFunc(int i, ...){ 2 va_list pArgs = NULL; 3 va_start(pArgs, i); 4 int j = va_arg(pArgs, int); 5 va_end(pArgs); 6 printf("i=%d, j=%d ", i, j); 7 }
分别采用以下三种方法调用:
1) IntegerVarArgFunc(10);
输出i=10, j=6803972(形参i的堆栈上方内容)
2) IntegerVarArgFunc(10, 20);
输出i=10, j=20,符合期望。
3) IntegerVarArgFunc(10, 20, 30);
输出i=10, j=20,多余的变参被忽略。
【示例2】函数通过固定参数指定可变参数个数,循环打印所有变参值。
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 ",dwArgIdx, dwArgVal); 10 } 11 va_end(pArgs); 12 }
调用方式为ParseVarArgByNum(3, 11, 22, 33);,输出:
The 1th Argument: 11
The 2th Argument: 22
The 3th Argument: 33
【示例3】函数定义一个结束标记,调用时通过最后一个参数传递该标记,以结束变参的遍历打印。
1 //最后一个参数作为变参结束符(-1),用于循环获取变参内容 2 void ParseVarArgByEnd(int dwStart, ...){ 3 va_list pArgs = NULL; 4 va_start(pArgs, dwStart); 5 int dwArgIdx = 0; 6 int dwArgVal = dwStart; 7 while(dwArgVal != -1){ 8 ++dwArgIdx; 9 printf("The %dth Argument: %d ",dwArgIdx, dwArgVal); 10 dwArgVal = va_arg(pArgs, int); //得到下个变参值 11 } 12 va_end(pArgs); 13 }
调用方式为ParseVarArgByEnd(44, 55, -1);,输出:
The 1th Argument: 44
The 2th Argument: 55
【示例4】函数自定义一些可能出现的参数类型,在变参列表中显式指定变参类型。可这样传递参数:参数数目,可变参数类型1,可变参数值1,可变参数类型2,可变参数值2,....。
1 //可变参数采用<ArgType, ArgValue>的形式传递,以处理不同的变参类型 2 typedef enum{ 3 CHAR_TYPE = 1, 4 INT_TYPE, 5 LONG_TYPE, 6 FLOAT_TYPE, 7 DOUBLE_TYPE, 8 STR_TYPE 9 }E_VAR_TYPE; 10 void ParseVarArgType(int dwArgNum, ...){ 11 va_list pArgs = NULL; 12 va_start(pArgs, dwArgNum); 13 14 int i = 0; 15 for(i = 0; i < dwArgNum; i++){ 16 E_VAR_TYPE eArgType = va_arg(pArgs, int); 17 switch(eArgType){ 18 case INT_TYPE: 19 printf("The %dth Argument: %d ", i+1, va_arg(pArgs, int)); 20 break; 21 case STR_TYPE: 22 printf("The %dth Argument: %s ", i+1, va_arg(pArgs, char*)); 23 break; 24 default: 25 break; 26 } 27 } 28 va_end(pArgs); 29 }
调用方式为ParseVarArgType(2, INT_TYPE, 222, STR_TYPE, "HelloWorld!");,输出:
The 1th Argument: 222
The 2th Argument: HelloWorld!
【示例5】实现简易的MyPrintf函数。该函数无返回值,即不记录输出的字符数目;接受"%d"按整数输出、"%c"按字符输出、"%b"按二进制输出,"%%"输出'%'本身。
1 char *MyItoa(int iValue, char *pszResBuf, unsigned int uiRadix){ 2 //If pszResBuf is NULL, string "Nil" is returned. 3 if(NULL == pszResBuf){ 4 //May add more trace/log output here 5 return "Nil"; 6 } 7 8 //If uiRadix(Base of Number) is out of range[2,36], 9 //empty resulting string is returned. 10 if((uiRadix < 2) || (uiRadix > 36)){ 11 //May add more trace/log output here 12 *pszResBuf = '