通常我们使用的C函数的参数个数都是固定的,但也有不固定的。比如printf()与scanf()。如何自己动手实现一个可变参数函数,这个还是有点技巧的。
我们最常用的就是定义一个宏,使用printf或者printk,如下
#define wwlogk(fmt, args...) printk(fmt, ## args)
现在我们自己动手实现一个可变参数的函数,后面分析原理。首先看一个例子:
#include <stdio.h>
#include <stdarg.h>
int Sum(int first, int second, ...)//当无法列出传递函数的所有实
//参的类型和数目时,可用省略号指定参数表
{
int sum = 0, t = first;
va_list vl;
va_start(vl, first);
while (t != -1){
sum += t;
t = va_arg(vl, int); //将当前参数转换为int类型
}
va_end(vl);
return sum;
}
int main(int argc, char* argv[])
{
printf("The sum is %d\n", Sum(30, 20, 10, -1)); //-1是参数结束标志
return 0;
}
在上面的例子中,实现了一个参数个数不定的求int型和的函数Sum()。
其中有几个变量需要说明一下。va_list、va_start()、va_end和va_arg。
Va_list:该类型变量用来访问可变参数,实际上就是指针。
Va_start():是一个宏,用来获取参数列表中的参数,使vl指向第一个可变参数,使用完毕后调用va_end()结束。
va_end:也是一个宏,用来结束va_start()的调用。
va_arg:宏,用来获参数列表中的取下一个值。
在linux源代码中,include/acpi/platform/acenv.h,头文件有详细描述。
1、 va_list vl;
typedef char *va_list; //定义了一个新的类型,指向字符串的指针。其实真实意图是当指针移动是以"1"单位,因为sizeof(char) =1;即char类型占一个字节,int型占4个字节。
2、 va_start(vl, first) 使vl指向第一个可变参数,即。
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
_bnd(X, bnd)的定义主要是为了某些需要内存的对齐的系统,这个宏的目的是为了得到最后一个固定参数的实际内存大小。直接用sizeof也没有影响。
#define _AUPBND (sizeof (acpi_native_int) - 1)
其中acpi_native_int是根据硬件平台来决定的,即一个int类型的宽度,即字节。
typedef s64 acpi_native_int;
typedef s32 acpi_native_int;
typedef int s32;
typedef long long s64;
下面就这段宏代码解释一下:
进程运行时,将变量压入栈,而(char *) &(A)指向first,即栈顶;
使用宏_bnd (A,_AUPBND),主要是为了某些系统需要内存按照整数字节对齐,因为C调用协议下面,参数入栈都是整数字节(指针或者值)--- 所谓对齐,对Intel80x86 机器来说就是要求每个变量的地址都是sizeof(int)的倍数。那为什么要对齐?因为在对齐方式下,CPU 的运行效率要快得多。
示例:如下图,当一个long 型数(如图中long1)在内存中的位置正好与内存的字边界对齐时,CPU 存取这个数只需访问一次内存,而当一个long 型数(如图中的long2)在内存中的位置跨越了字边界时,CPU 存取这个数就需要多次访问内存,如i960cx 访问这样的数需读内存三次(一个BYTE、一个SHORT、一个BYTE,由CPU 的微代码执行,对软件透明),所以对齐方式下CPU 的运行效率明显快多了。
1 8 16 24 32
------- ------- ------- ---------
| long1 | long1 | long1 | long1 |
------- ------- ------- ---------
| | | | long2 |
------- ------- ------- ---------
| long2 | long2 | long2 | |
------- ------- ------- ---------
因为_AUPBND为int宽度,那么_bnd(A, _AUPBND)的意思就是不够一个int宽度的数据,将还是跳过一个int宽度。比如char、short类型的数据sizeof后为1和2,假设现在是32位系统char类型,_bnd(X, bnd) = (1 + 3 ) & (~3) = 0x4; 2也一样。因为32位系统int宽度为4。跳过以后,ap指向second,而first的值已经保存在t中。这里需要说明的是,因为题设已经指定first为int型。若为其他型(如char)则会出现错误,因为这里首先跳过了4个字节。
图1 栈的结构
另外这里还需要注意几点:
- 因为C语言压栈顺序为从右到左。
- 栈的扩展方向是向下扩展,所以栈底为高地址,栈顶为低地址。
比如假设f(a,b,c,d)按照从右到左压栈,那么d应该是第一个进栈的,a是最后一个进栈的,所以d的地址应该比a的高。在intel+ windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
- C语言压栈的时候,第一个进栈的是主函数中第一条指令的地址,然后依次是函数参数和局部变量。
如上所述,va_start(vl,first)后,vl指向first后面的第一个可变参数。我们都知道Pascal的参数入栈顺序时自左向右的,但是C语言会是自右向左。为什么呢?这也是C语言比pascal高级的一个地方--C语言通过这种参数入栈的顺序实现了对变长参数函数的支持!
为了支持可变参数函数,C语言引入新的调用协议, 即C语言调用约定 __cdecl . 采用C/C++语言编程的时候,默认使用这个调用约定。如果要采用其它调用约定,必须添加其它关键字声明,例如WIN32 API使用PASCAL调用约定,函数名字之前必须加__stdcall关键字。 采用C调用约定时,函数的参数是从右到左入栈,个数可变。由于函数体不能预先知道传进来的参数个数,因此采用本约定时必须由函数调用者负责堆栈清理。
3、#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
把这个宏展开可以看的更清楚:
- ap = ap + _bnd(T,_AUPBND) --首先将ap的跳过指定宽度,即指向下一个可变参数。
- *(T *)(ap - _bnd(T,_AUPBND)) --然后还原ap后,将其转化为"T"型指针并求指针的值。
现在可以看清楚这个宏的意思是求当前ap指向的值,并将ap指向下一目标。
4、va_end(vl) 把vl指针清为NULL
#define va_end(ap) (void) 0
注意:这段代码只有在windows下编译和运行后,才能输出正确的结果,在linux下,需要将int Sum(int first, int second, ...)修改为int Sum(int first, ...),即删掉int second,因为second如果写出来,就不是可变参数,first也不是可变参数。修改后的函数在linux下和windows下都可以正常运行。
搞清楚上面示例代码的原理后,我们可以自己手动实现这样一个函数。
#include <stdio.h>
int Sum(int first, int second,...)
{
int sum = 0, t = first;
char * vl;//定义一个指针
vl = (char *)&first;//使指针指向第一个参数
while (*vl != -1)//-1是预先给定的结束符
{
sum += *(int *)vl;//类型转换
vl += sizeof(int);//移动指针,使指针指向下一个参数
}
return sum;
}
int main(int argc, char* argv[])
{
printf("The sum is %d\n", Sum(30, 20, 10, -1));//-1是参数结束标志
return 0;
}
实际上声明一个可变参数有两种方式:建议使用第一种。
第一种:包含头文件stdarg.h,采用ANSI标准形式,参数个数可变的函数的原型声明是:
type funcname(type para1, type para2, ...)
第二种:包含头文件varargs.h,采用与UNIX System V兼容的声明方式时,参数个数可变的函数原型是:
type funcname(va_alist)
va_dcl
va_dcl为宏,宏定义原型后已经包含分号,所以使用时不用加分号。Va_dcl是对va_alist的详细声明。Va_dcl在代码中必须原样给 出,va_alist在VC中可以原样给出,也可以略去,但在UNIX上的CC或Linux上的GCC中都要省略掉。
关于可变参数的传递问题
有人问到这个问题,假如我定义了一个可变参数函数,在这个函数内部又要调用其它可变参数函数,那么如何传递参数呢?上面的例子都是使用宏va_arg逐个把参数提取出来使用,能否不提取,直接把它们传递给另外的函数呢?
我们先看printf的实现:
int __cdecl printf (const char *format, ...)
{
va_list arglist;
int buffing;
int retval;
va_start(arglist, format); //arglist指向format后面的第一个参数
...//不关心其它代码
retval = _output(stdout,format,arglist); //把format格式和参数传递给output函数
...//不关心其它代码
return(retval);
}
我们先模仿这个函数写一个:
#include <stdio.h>
#include <stdarg.h>
int mywrite(char *fmt, ...)
{
va_list arglist;
va_start(arglist, fmt);
return printf(fmt,arglist);
}
void main()
{
int i=10, j=20;
char buf[] = "This is a test";
double f= 12.345;
mywrite("String: %s\nInt: %d, %d\nFloat :%4.2f\n", buf, i, j, f);
}
运行一下看看,错误百出。仔细分析原因,根据宏的定义我们知道 arglist是一个指针,它指向第一个可变的参数,但是所有的参数都位于栈中,所以arglist指向栈中某个位置,通过arglist的值,我们可以直接查看栈里面的内容:
arglist -> 指向栈里面,内容包括
0067FD78 E0 FD 67 00 //指向字符串"This is a test"
0067FD7C 0A 00 00 00 //整数 i 的值
0067FD80 14 00 00 00 //整数 j 的值
0067FD84 71 3D 0A D7 //double 变量 f, 占用8个字节
0067FD88 A3 B0 28 40
0067FD8C 00 00 00 00
如果直接调用 printf(fmt, arglist); 仅仅是把arglist指针的值0067FD78入栈,然后把格式字符串入栈,相当于调用:
printf(fmt, 0067FD78);
自然这样的调用肯定会出现错误。
我们能不能逐个把参数提取出来,再传递给其它函数呢?先考虑一次性把所有参数传递进去的问题。
如果调用的是系统库函数,这种情况下是不可能的。因为提取参数是在运行态,而参数入栈是在编译的时候确定的。无法让编译器预知运行态的事情给出正确的参数入栈代码。而我们在运行态虽然可以提取每个参数,但是无法将参数一次性全部压栈,即使使用汇编代码实现起来也是很困难的,因为不单是一个简单的push代 码就可以做到。
---------------------------------------------------------
问题一:
上面这段代码经测试可以正常输出。也就是说,我们通过使用指针,实现了参数不定的函数。但这里还有一个问题,就是sum函数的所有参数都是int类型的,事先我们知道要移动sizeof(int)位的指针,可是如果参数类型不同呢?
答案与分析:这的确是个比较麻烦的问题,因为不同的数据类型占用的字节数可能是不一样的(如double型为8个字符,short int型为2个),所以很难事先确定应该移动多少个字节!但是办法还是有的,这就是使用指针了,无论什么类型的指针,都是占用4个字节,所以,可以把所有的传如入参数都设置为指针,这样一来,就可以通过移动固定的4个字节来实现遍历可变参数的目的了,至于如何取得指针中的内容并使用他们,当然也是无法预先得知的了。所以这大概也就是像printf(),scanf()之类的函数还需要一个格式控制符的原因吧^_^!不过实现起来还是有不少麻烦,暂且盗用vprintf()来实现一个与printf()函数一样功能的函数了,代码如下:
void myPrint(const char *frm, ...)
{
va_list vl;
va_start(vl, frm);
vprintf(frm, vl);
va_end(vl);
}
-----------------------------------------------------------
问题二: 还有一个问题,是上述问题的变体,不过意思相同:有没有办法写一个函数,这个函数参数的具体形式可以在运行时才确定?
答案与分析:目前没有"正规"的解决办法,不过独门偏方倒是有一个,因为有一个函数已经给我们做出了这方面的榜样,那就是main(),它的原型是:
int main(int argc,char *argv[]);
函数的参数是argc和argv。
深入想一下,"只能在运行时确定参数形式",也就是说你没办法从声明中看到所接受的参数,也即是参数根本就没有固定的形式。常用的办法是你可以通过定 义一个void *类型的参数,用它来指向实际的参数区,然后在函数中根据根据需要任意解释它们的含义。这就是main函数中argv的含义,而argc,则用来表明实际的参数个数,这为我们使用提供了进一步的方便,当然,这个参数不是必需的。
虽然参数没有固定形式,但我们必然要在函数中解析参数的意义,因此,理所当然会有一个要求,就是调用者和被调者之间要对参数区内容的格式,大小,有效性等所有方面达成一致,否则南辕北辙各说各话就惨了。
-------------------------------------------------------------
问题三:可变长参数的传递
有时候,需要编写一个函数,将它的可变长参数直接传递给另外的函数,请问,这个要求能否实现?
答案与分析:目前,你尚无办法直接做到这一点,但是我们可以迂回前进,首先,我们定义被调用函数的参数为va_list类型,同时在调用函数中将可变长参数列表转换为va_list,这样就可以进行变长参数的传递了。看如下所示:
void subfunc (char *fmt, va_list argp)
{
...
arg = va_arg (fmt, argp); /* 从argp中逐一取出所要的参数 */
...
}
void mainfunc (char *fmt, ...)
{
va_list argp;
va_start (argp, fmt); /* 将可变长参数转换为va_list */
subfunc (fmt, argp); /* 将va_list传递给子函数 */
va_end (argp);
...
}
-------------------------------------------------------------
问题四:如何判别可变参数函数的参数类型?
函数形式如下:
void fun(char* str,...)
{
......
}
若传的参数个数大于1,如何判别第2个以后传参的参数类型???最好有源码说明!
答案与分析:无法判断。可变参数实现主要通过三个宏实现:va_start,va_arg,va_end。
如楼上所说,例如printf( "%d%c%s ", ....)是通过格式串中的%d,%c,%s来确定后面参数的类型,其实你也可以参考这种方法来判断不定参数的类型。
-------------------------------------------------------------
问题五:定义可变长参数的一个限制
为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数?
int f (...)
{
...
}
答案与分析:不可以。这是ANSI C 所要求的,你至少得定义一个固定参数。
这个参数将被传递给va_start(),然后用va_arg()和va_end()来确定所有实际调用时可变长参数的类型和值。
---------------------------------------------------------------------
问题六:可变长参数的获取
有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:
va_arg (argp, float);
这样做可以吗?
答案与分析:不可以。在可变长参数中,应用的是"加宽"原则。也就是float类型被扩展成double;char, short被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对char和short类型的则用va_arg(argp, int)。
---------------------------------------------------------------------
问题七:可变长参数中类型为函数指针
我想使用va_arg来提取出可变长参数中类型为函数指针的参数,结果却总是不正确,为什么?
答案与分析:这个与va_arg的实现有关。一个简单的、演示版的va_arg实现如下:
#define va_arg(argp, type) (*(type *)(((argp) += sizeof(type)) - sizeof(type)))
其中,argp的类型是char *。
如果你想用va_arg从可变参数列表中提取出函数指针类型的参数,例如
int (*)(),则va_arg(argp, int (*)())被扩展为:
(*(int (*)() *)(((argp) += sizeof (int (*)())) -sizeof (int (*)())))
显然,(int (*)() *)是无意义的。
解决这个问题的办法是将函数指针用typedef定义成一个独立的数据类型,例如:
typedef int (*funcptr)();
这时候再调用va_arg(argp, funcptr)将被扩展为:
(* (funcptr *)(((argp) += sizeof (funcptr)) - sizeof (funcptr)))
这样就可以通过编译检查了。
知识扩展
可能大家也猜到了,我扩展要扩展什么了?!^_^
简单介绍两种函数调用约定
__stdcall (C++默认)
- 参数从右向左压入堆栈
- 函数被调用者修改堆栈
- 函数名(在编译器这个层次)自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸
__cdecl (C语言默认)
- 参数从右向左压入堆栈
- 参数由调用者清楚,手动清栈,被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。
那么,变参函数的调用方式为(也只能是):__cdecl 。