可变参数函数又称参数个数可变函数(本文也简称变参函数),即函数参数数目可变。原型声明格式为:
type VarArgFunc(type FixedArg1, type FixedArg2, …); |
其中,参数可分为两部分:数目确定的固定参数和数目可变的可选参数。函数至少需要一个固定参数,其声明与普通函数参数相同;可选参数由于数目不定(0个或以上),声明时用"…"表示(“…”用作参数占位符)。固定参数和可选参数共同构成可变参数函数的参数列表。
由于参数数目不定,使用可变参数函数通常能缩短编码,灵活性和易用性较高。
典型的变参函数如printf(及其家族),其函数原型为:
int printf(const char* format, ...); |
printf函数除参数format固定外,后续参数的数目和类型均可变。实际调用时可有以下形式:
printf("string"); |
1 变参函数实现原理
C调用约定下可使用va_list系列变参宏实现变参函数,此处va意为variable-argument(可变参数)。典型用法如下:
#include <stdarg.h> |
变参宏的定义和实现因操作系统、硬件平台及编译器而异(但原理相似)。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; |
①_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)) |
②va_start宏根据(va_list)&v得到第一个可变参数前的一个固定参数在堆栈中的内存地址,加上_INTSIZEOF(v)即v所占内存大小后,使ap指向固定参数后下个参数(第一个可变参数地址)。
固定参数的地址用于va_start宏,因此不能声明为寄存器变量(地址无效)或作为数组类型(长度难定)。
③va_arg宏取得type类型的可变参数值。首先ap+=_INTSIZEOF(type),即ap跳过当前可变参数而指向下个变参的地址;然后ap-_INTSIZEOF(type)得到当前变参的内存地址,类型转换后返回当前变参值。
va_arg宏的等效实现如下
//将指针移动至下个变参,并返回左移的值[-1](数组下标表示偏移量),即当前变参值 |
函数内可多次遍历可变参数,但每次必须以va_start宏开始,因为遍历后ap指针不再指向首个变参。
下图给出基于变参宏的可变参数在堆栈中的分布:
变参宏无法智能识别可变参数的数目和类型,因此实现变参函数时需自行判断可变参数的数目和类型。前者可显式提供变参数目或设定遍历结束条件(如-1、'\0'或回车符等)。后者可显式提供变参类型枚举值,或在固定参数中包含足够的类型信息(如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\n", 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\n",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\n",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\n", i+
1, va_arg(pArgs,
int));
-
20
break;
-
21
case STR_TYPE:
-
22
printf(
"The %dth Argument: %s\n", 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 =
'\0';
-
13
return pszResBuf;
-
14 }
-
15
-
16
char *pStr = pszResBuf;
//Pointer to traverse string
-
17
char *pFirstDig = pszResBuf;
//Pointer to first digit
-
18
if((
10 == uiRadix) && (iValue <
0)){
//Negative decimal number
-
19 iValue = (
unsigned
int)-iValue;
-
20 *pStr++ =
'-';
-
21 pFirstDig++;
//Skip negative sign
-
22 }
-
23
-
24
int iTmpValue =
0;
-
25
do{
-
26 iTmpValue = iValue;
-
27 iValue /= uiRadix;
-
28
//Calculating the modulus operator(%) by hand saving a division
-
29 *pStr++ =
"0123456789abcdefghijklmnopqrstuvwxyz"[iTmpValue - iValue * uiRadix];
-
30 }
while(iValue);
-
31 *pStr-- =
'\0';
//Terminate string, pStr points to last digit(or negative sign)
-
32
//Now have a string of number in reverse order
-
33
-
34
//Swap *pStr and *pFirstDig for reversing the string of number
-
35
while(pFirstDig < pStr){
//Repeat until halfway
-
36
char cTmpChar = *pStr;
-
37 *pStr--= *pFirstDig;
-
38 *pFirstDig++ = cTmpChar;
-
39 }
-
40
return pszResBuf;
-
41 }
-
42
-
43
void MyPrintf(const char *pszFmt, ... ){
-
44 va_list pArgs =
NULL;
-
45 va_start(pArgs, pszFmt);
-
46
-
47
for(; *pszFmt !=
'\0'; ++pszFmt){
-
48
//若不是控制字符则原样输出字符
-
49
if(*pszFmt !=
'%'){
-
50
putchar(*pszFmt);
-
51
continue;
-
52 }
-
53
-
54
//若是控制字符则查看下一字符
-
55
switch(*++pszFmt){
-
56
case
'%':
//连续两个'%'输出单个'%'
-
57
putchar(
'%');
-
58
break;
-
59
case
'd':
//按照整型输出
-
60
printf(
"%d", va_arg(pArgs,
int));
-
61
break;
-
62
case
'c':
//按照字符输出
-
63
printf(
"%c", va_arg(pArgs,
int));
//不可写为...va_arg(pArgs, char);
-
64
break;
-
65
case
'b': {
//按照二进制输出
-
66
char aucStr[
sizeof(
int)*
8 +
1] = {
0};
-
67
fputs(MyItoa(va_arg(pArgs,
int), aucStr,
2),
stdout);
-
68
//printf(MyItoa(va_arg(pArgs, int), aucStr, 2));
-
69
break;
-
70 }
-
71
default:
-
72
vprintf(--pszFmt, pArgs);
-
73
return;
-
74 }
-
75 }
//end of for-loop
-
76 va_end(pArgs);
-
77 }
调用方式为MyPrintf("Binary string of number %d is = %b!\n", 9999, 9999);,输出:
Binary string of number 9999 is = 10011100001111!
注意,MyPrintf函数for循环语句段旨在自定义格式化输出(如%b),而非实现printf库函数本身;否则直接使用vprintf(pszFmt, pArgs);即可。此外该函数存在一处明显缺陷,即%b前若出现case匹配项外的控制字符(如%x),则会调用vprintf函数处理该字符及其后的格式串,%b将会原样输出"%b"(而非转换为二进制)。
本示例中也附带实现了MyItoa函数。该函数与非标准C语言扩展函数itoa功能相同。该函数将整数iValue转换为uiRadix 所指定的进制数字符串,并将其存入pszResBuf字符数组。
【示例6】可变参数数目不多时,可用数组或结构体数组变相实现可变参数函数。
-
#define VAR_ARG_MAX_NUM (unsigned char)10
-
#define VAR_ARG_MAX_LEN (unsigned char)20
-
//可变参数信息
-
typedef
struct{
-
E_VAR_TYPE eArgType;
-
unsigned
char aucArgVal[VAR_ARG_MAX_LEN];
-
}VAR_ARG_ENTRY;
-
typedef
struct{
-
unsigned
char ucArgNum;
-
VAR_ARG_ENTRY aucVarArg[VAR_ARG_MAX_NUM];
-
}VAR_ARG_LIST;
-
-
void ParseStructArrayArg(VAR_ARG_LIST *ptVarArgList){
-
int i =
0;
-
for(i =
0; i < ptVarArgList->ucArgNum; i++){
-
E_VAR_TYPE eArgType = ptVarArgList->aucVarArg[i].eArgType;
-
switch(eArgType){
-
case CHAR_TYPE:
-
printf(
"The %dth Argument: %c\n", i+
1, ptVarArgList->aucVarArg[i].aucArgVal[
0]);
-
break;
-
case STR_TYPE:
-
printf(
"The %dth Argument: %s\n", i+
1, ptVarArgList->aucVarArg[i].aucArgVal);
-
break;
-
default:
-
break;
-
}
-
}
-
}
调用方式为
VAR_ARG_LIST tVarArgList = {2, {{CHAR_TYPE, {'H'}}, {STR_TYPE, "TEST"}}}; |
The 1th Argument: H
The 2th Argument: TEST
本示例函数原型稍加改造,显式声明参数数目如下:
void ParseStructArrayArg(unsigned char ucArgNum, VAR_ARG_ENTRY aucVarArg[]);或 |
int main(int argc, char *argv[]);或 |
通过数组可替代某些不必要的变参函数实现,如对整数求和:
实现方式 | 可变参数函数 | 数组替代 |
函数代码 | int SumVarArg(int dwStart, ...){ |
3 变参函数注意事项
可变参数函数在编程中应注意以下问题:
1) 编译器对可变参数函数的原型检查不够严格,不利于编程查错。
调用变参函数时,传递的变参数目应不少于该函数所期望的变参数目(该数目由主调函数实参指定或由变参函数内部实现决定),否则会访问到函数参数以外的堆栈区域,可能导致堆栈错误。
如示例1中可变参数为char*类型(用%s打印) 时,若使用整型变参调用该函数,可能会出现段错误(Linux)或页面非法错误(Windows),也可能出现难以觉察的细微错误。
printf函数格式化字符串参数所指定的类型与后面变参的类型不匹配时,也可能造成程序崩溃(尤其以%s打印整型参数值时)。
gcc编译器提供attribute 机制用以编译时检查某些变参函数调用情况,如声明函数为
void OmciLog(LOG_TYPE eLogType, const char *pFmt, ...) __attribute__((format(printf,2,3))); |
表示函数原型中第2个参数(pFmt)为格式化字符串,从参数列表中第3个参数(即首个变参)开始与pFmt形式比较。该声明将对OmciLog(LOG_PON, "%s", 1)的调用产生编译警告:
VarArgs.c:204: warning: format '%s' expects type 'char *', but argument 3 has type 'int' |
但该机制主要针对类似scanf/printf的变参函数,此类函数可根据格式化字符串确定变参数目和类型。
2) va_arg(ap, type)宏获取变参时,type不可指定为以下类型:
- char、signed char、unsigned char
- short、unsigned short
- signed short、short int、signed short int、unsigned short int
- float
在C语言中,调用不带原型声明或声明为变参的函数时,主调函数会在传递未显式声明的参数前对其执行“缺省参数提升(default argument promotions)”,将提升后的参数值传递给被调函数。
提升操作如下:
- float类型的参数提升为double类型
- char、short和相应的signed、unsigned类型参数提升为int类型
- 若int类型不能存储原值,则提升为unsigned int类型
在gcc 编译器中,若type使用char或unsigned short int等需提升的类型,可能会得到严重警告。
因此,若要获取变参数列表中float类型的实参,则变参函数中应使用double dVar = va_arg(ap, double)或float fVar = (float)va_arg(ap, double)。char和short类型实参处理方式与之类似。
3) 使用va_arg宏获取变参列表中类型为函数指针的参数时,可能需要将函数指针用typedef定义为新的数据类型,以便通过编译(与va_arg宏的实现有关)。
对于VC6.0的va_arg宏实现,若用该宏从变参列表中提取函数指针类型的参数,如
va_arg(argp, int(*)()); |
被扩展为以下形式(为缩减长度直接写出_INTSIZEOF宏值)
( *(int (*)() *)((pArgs += 4) - 4) ); |
显然,(int (*)() *)无意义。
解决方法如下
typedef int (*pFunc)(); |
va_arg(argp, pFunc)被扩展为(*(pFunc *)((pArgs += 4) - 4)),即可通过编译检查。
而在gcc编译器下,va_arg宏可直接使用函数指针类型。
-
1
//for Gcc Compiler
-
2
int DummyFunc(void){
printf(
"Here!!!\n");
return
0; }
-
3
void ParseFuncPtrVarArg(int i, ...){
-
4 va_list pArgs =
NULL;
-
5 va_start(pArgs, i);
-
6
char *sVal = va_arg(pArgs,
char*);
-
7 va_end(pArgs);
-
8
printf(
"%d %s ", i, sVal);
-
9
-
10
int (*pf)() = va_arg(pArgs,
int (*)());
-
11 pf();
-
12 }
以ParseFuncPtrVarArg(1, "Welcome", DummyFunc);方式调用,输出为1 Welcome Here!!!。
4) C语言层面上无法将函数A的可变参数直接传递给函数B。只能定义被调函数的参数为va_list类型,在主调函数中将可变参数列表转换为va_list,再进行可变参数的传递。这种技巧常用于定制打印函数:
-
1
INT32S OmciLog(E_LOG_TYPE eLogType, const CHAR *pszFmt, ...){
-
2 CHECK_SINGLE_POINTER(pFormat, RETURN_VOID);
-
3
-
4
if(
0 == GET_BIT(gOmciLogCtrl, eLogType))
-
5
return;
-
6
-
7 CHAR aucLogBuf[OMCI_LOG_BUF_LEN] = {
0};
-
8 va_list pArgs =
NULL;
-
9 va_start(pArgs, pszFmt);
-
10 INT32S dwRetVal = vsnprintf(aucLogBuf,
sizeof(aucLogBuf), pszFmt, pArgs);
-
11 va_end(pArgs);
-
12
-
13 OUTPUT_LOG(aucLogBuf);
-
14
return dwRetVal;
-
15 }
其中被调函数vsnprintf可根据va_arg(pszFmt, pArgs)依次取出所需的变参。
以OmciLog("%d %f %s\n", 10, 20.3, "ABC");方式调用,输出为10 20.300000 ABC。
5) 可变参数必须从头到尾按照顺序逐个访问。可访问几个变参后中止,但不能一开始就访问变参列表中间的参数。
6) ANSI C要求至少定义一个固定参数(ISO C requires a named argument before '...'),该参数将传递给va_start宏以查找参数列表的可变部分。故不可定义void func(...)这样的函数。
7) 变参宏实现与堆栈相关,在参数入寄存器的处理器下实现可能异常复杂(gcc中va_start宏会将所有可能用于变参传递的寄存器均保存在栈中)。因此如非必要,应尽量避免使用变参宏。C语言中除示例6中数组或结构体数组替代方式外,还可采用回调函数方式"抛出"变化部分,如:
-
1
/**********************************************************************
-
2 * 函数名称: OmciLocateListNode
-
3 * 功能描述: 查找链表首个与pData满足函数fCompareNode判定关系的结点
-
4 * 输入参数: T_OMCI_LIST* pList :链表指针
-
5 * VOID* pData :待比较数据指针
-
6 * CompareNodeFunc fCompareNode :比较回调函数指针
-
7 * 输出参数: NA
-
8 * 返 回 值: T_OMCI_LIST_NODE* 链表结点指针(未找到时返回NULL)
-
9 ***********************************************************************/
-
10 T_OMCI_LIST_NODE* OmciLocateListNode(T_OMCI_LIST *pList, VOID *pData, CompareNodeFunc fCompareNode)
-
11 {
-
12 CHECK_TRIPLE_POINTER(pList, pData, fCompareNode,
NULL);
-
13 CHECK_SINGLE_POINTER(pList->pHead,
NULL);
-
14 CHECK_SINGLE_POINTER(pList->pHead->pNext,
NULL);
-
15
-
16
if(
0 == pList->dwNodeNum)
-
17 {
-
18
return
NULL;
-
19 }
-
20
-
21 T_OMCI_LIST_NODE *pListNode = pList->pHead->pNext;
-
22
while(pListNode != pList->pHead)
-
23 {
-
24
if(
0 == fCompareNode(pListNode->pNodeData, pData, pList->dwNodeDataSize))
-
25
return pListNode;
-
26
-
27 pListNode = pListNode->pNext;
-
28 }
-
29
-
30
return
NULL;
-
31 }
OmciLocateListNode函数是下面Omci_List_Query函数的另一实现。主调函数提供fCompareNode回调函数以比较链表结点,从而简化代码实现,并增强可读性。
-
1
/***************************************************************
-
2 * Function: Omci_List_Query
-
3 * Description -
-
4 * 根据给定的KEY偏移和KEY长度,查找目标节点
-
5 * Input:
-
6 * pList: 链表
-
7 * 可变参数: 三个参数为一组,第一个为key value,第二个为key
-
8 * 偏移,第三个为key长度,以LIST_END表示参数结束。
-
9 * Output:
-
10 * Returns:
-
11 *
-
12 * modification history
-
13 * -------------------------------
-
14 * Created : 2011-5-25 by xxx
-
15 * ------------------------------
-
16 ***************************************************************/
-
17
OMCI_LIST_NODE* Omci_List_Query(OMCI_LIST *pList, ...)
-
18 {
-
19 OMCI_LIST_NODE_KEY aKeyGroup[MAX_LIST_NODE_KEYS_NUM];
-
20 OMCI_LIST_NODE *pNode=
NULL;
-
21 INT8U *pData=
NULL, *pKeyValue=
NULL;
-
22 INT8U ucKeyNum=
0, i;
-
23 INT32U iKeyOffset=
0, iKeyLen=
0;
-
24 VA_LIST tArgList;
-
25
-
26
if(
NULL==pList)
-
27
return
NULL;
-
28
memset((INT8U*)aKeyGroup,
0,
sizeof(OMCI_LIST_NODE_KEY)*MAX_LIST_NODE_KEYS_NUM);
-
29 VA_START(tArgList, pList);
-
30
while(TRUE)
-
31 {
-
32 pKeyValue=VA_ARG(tArgList, INT8U*);
-
33
if(LIST_END==pKeyValue)
-
34
break;
-
35 iKeyOffset=VA_ARG(tArgList, INT32U);
-
36 iKeyLen=VA_ARG(tArgList, INT32U);
-
37
if(
0==iKeyLen)
-
38 {
-
39 VA_END(tArgList);
-
40
return
NULL;
-
41 }
-
42
if(ucKeyNum>=MAX_LIST_NODE_KEYS_NUM)
-
43 {
-
44 VA_END(tArgList);
-
45
return
NULL;
-
46 }
-
47 aKeyGroup[ucKeyNum].pKeyValue=pKeyValue;
-
48 aKeyGroup[ucKeyNum].iKeyOffset=iKeyOffset;
-
49 aKeyGroup[ucKeyNum++].iKeyLen=iKeyLen;
-
50 }
-
51 VA_END(tArgList);
-
52
-
53 pNode=Omci_List_First(pList);
-
54
while(
NULL!=pNode)
-
55 {
-
56 pData=(INT8U*)pNode->pNodeData;
-
57
for(i=
0; i<ucKeyNum; i++)
-
58 {
-
59
if(
0!=
memcmp(&pData[aKeyGroup[i].iKeyOffset], aKeyGroup[i].pKeyValue, aKeyGroup[i].iKeyLen))
-
60
break;
-
61 }
-
62
if(i>=ucKeyNum)
-
63 {
-
64
break;
-
65 }
-
66 pNode=pNode->pNext;
-
67 }
-
68
return pNode;
-
69 }
在C++语言里,可利用多态性来实现可变参数的功能(但灵活性有所下降)。
【扩展阅读】vsnprintf函数 |