在c语言中使用变长参数最常见的就是下面两个函数了:
int printf(const char *format, ...); int scanf(const char *format, ...);
那他们是怎样实现支持变成参数的呢?在使用变长参数的函数(这里假设是func)实现部分其实用到了stdarg.h里面的多个宏来访问那些不确定的参数,它们分别是:
void va_start(va_list ap, last); type va_arg(va_list ap, type); void va_end(va_list ap);
假设lastarg是func的最后一个具名参数,即在func函数定义中...之前的那个参数(在printf中lastarg是format,printf(format,...)),在func中首先定义一个变量:
va_list ap
这个变量以后会依次指向各个可变参数。ap在使用之前必须用宏va_start初始化一次,如下所示:
va_start(ap, lastarg);
其中lastarg是func中的最后一个具名参数。然后就可以用va_arg来获得下一个不定参数(前提是知道这个不定参数的类型type):
type next = va_arg(ap, type)
最后就是用宏va_end来清理现场。
下面我们来自己实现一个可变参数的函数:
一定要包含头文件:
stdarg.h
#include<stdio.h> #include<stdarg.h> void func(char *fmt,...) { va_list ap; va_start(ap,fmt); while(*fmt) { switch(*fmt) { case 'd': fprintf(stdout,"%d ",(int)va_arg(ap,int)); break; case 'c': fprintf(stdout,"%c ",(char)va_arg(ap,char)); break; case 's': fprintf(stdout,"%s ",(char*)va_arg(ap,char *)); break; default: fprintf(stderr,"error fmt "); } fmt++; } va_end(ap); } int main() { func("dcs",10,'s',"hello world"); }
输出结果:
10 s hello
可以看到上面的程序完成按我们的意愿实现了变长参数的访问,通过前面的fmt来控制下一个不定参数的类型。那这三个宏有事怎样实现我们对不定参数的访问的呢?下面我们来看看它们是怎么实现的:
va_list实际就是一个指向各个不定参数的指针,由于参数的类型是不确定了,所以可以定义va_list为void *或者char *类型,(定义成void*可能会提示表达式必须是指向完整对象的指针 ,定义成char*比较好),即
#define va_list void *
va_start就是将va_list指向函数最后一个具名参数lastarg后面的位置,这个位置就是第一个不定参数。
#define va_start(ap, lastarg) (ap = (va_list)&lastarg + sizeof(lastarg))
va_arg获取当前不定参数的值,根据当前参数的类型的大小移动指针指向下一个不定参数。
#define va_arg(ap, type) (*(type *)((ap += sizeof(type)) - sizeof(type)))
ap += sizeof(type)) - sizeof(type) 怎么理解 ,首先ap=ap+sizeof(type) 把ap向后移动一个单位,然后减去-sizeof(type),注意这里没有改变ap的值,而且返回结果,这样保证
每次调用va_arg后,ap都会向后移动一个单位。
(上面的va_arg处理%s这样的是有问题的,一运行就会崩溃,
/*my implement */ typedef char* va_list_my;//不要定义成void* #define va_start_my(ap,lastarg) (ap = (va_list)&lastarg + sizeof(lastarg)) #define va_arg_my(ap, t) (*(t *)((ap += sizeof(t)) - sizeof(t))) //( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) // #define va_end_my(ap) (ap=(va_list_my)0) void funcMy(char *fmt, ...) { va_list_my ap; va_start_my(ap, fmt); while (*fmt) { switch (*fmt) { case 'd': fprintf(stdout, "%d ", (int)va_arg(ap, int)); break; case 'c': fprintf(stdout, "%c ", (char)va_arg_my(ap, char)); break; case 's': fprintf(stdout, "%s ", (char*)va_arg_my(ap, char*)); break; default: fprintf(stderr, "error fmt "); } fmt++; } va_end_my(ap); }
上面为什么会崩溃,找到原因了,
fprintf(stdout, "%c
", (char)va_arg_my(ap, char));
这里只移动了一个char, 如果我把char改成int,就可以运行了。
由于内存对齐,编译器在栈上压入参数时,不是一个紧挨着另一个的,编译器会根据变参的类型将其放到满足类型对齐的地址上的,这样栈上参数之间实际上可能会是有空隙的
改成:
#define va_arg_my(ap, t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 可以运行。
问题出在了我们不应该用sizeof,而应该用_INTSIZEOF.
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
一篇文章:
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
1 我们知道对于IX86,sizeof(int)一定是4的整数倍,所以~(sizeof(int) - 1) )的值一定是
右面[sizeof(n)-1]/2位为0,整个这个宏也就是保证了右面[sizeof(n)-1]/2位为0,其余位置
为1,所以_INTSIZEOF(n)的值只有可能是4,8,16,......等等,实际上是实现了字节对齐。
2 #define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
的目的在于把sizeof(n)的结果变成至少是sizeof(int)的整倍数,这个一般用来在结构中实现按int的倍数对齐。
如果sizeof(int)是4,那么,当sizeof(n)的结果在1~4之间是,_INTSIZEOF(n)的结果会是4;当sizeof(n)的结果在5~8时,
_INTSIZEOF(n)的结果会是8;当sizeof(n)的结果在9~12时,_INTSIZEOF(n)的结果会是12;……总之,会是sizeof(int)的倍数。
理论基础:
对于两个正整数 x, n 总存在整数 q, r 使得
x = nq + r, 其中 0<= r <n //最小非负剩余
q, r 是唯一确定的。q = [x/n], r = x - n[x/n]. 这个是带余除法的一个简单形式。在 c 语言中, q, r 容易计算出来: q = x/n, r = x % n.
所谓把 x 按 n 对齐指的是:若 r=0, 取 qn, 若 r>0, 取 (q+1)n. 这也相当于把 x 表示为:
x = nq + r', 其中 -n < r' <=0 //最大非正剩余
nq 是我们所求。关键是如何用 c 语言计算它。由于我们能处理标准的带余除法,所以可以把这个式子转换成一个标准的带余除法,然后加以处理:
x+n = qn + (n+r'),其中 0<n+r'<=n //最大正剩余
x+n-1 = qn + (n+r'-1), 其中 0<= n+r'-1 <n //最小非负剩余
所以 qn = [(x+n-1)/n]n. 用 c 语言计算就是:
((x+n-1)/n)*n
若 n 是 2 的方幂, 比如 2^m,则除为右移 m 位,乘为左移 m 位。所以把 x+n-1 的最低 m 个二进制位清 0就可以了。得到:
(x+n-1) & (~(n-1))
)。
va_end将指针清0。
#define va_end(ap) (ap=(va_list)0)
本质上其实就是靠前面lastvar来控制不定参数的类型,va_list变量来指向不定参数的地址,然后根据lastvar一个一个的获取不定参数。
vs中定义:
1: ///stdarg.h 2: #define va_start _crt_va_start 3: #define va_arg _crt_va_arg 4: #define va_end _crt_va_end 5: 6: ///vadefs.h 7: #define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) ) 8: typedef char * va_list; 9: #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) 10: #define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) ) 11: #define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 12: #define _crt_va_end(ap) ( ap = (va_list)0 )
转自:http://www.cnblogs.com/chengxuyuancc/archive/2013/05/30/3107058.html
首先在介绍可变参数表函数的设计之前,我们先来介绍一下最经典的可变参数表printf函数的实现原理。
一、printf函数的实现原理
在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来,在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,下面给出printf("%d,%d",a,b);(其中a、b都是int型的)的汇编代码
.section
.data
string out = "%d,%d"
push b
push a
push $out
call printf
你会看到,参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。
二、可变参数表函数的设计
标准库提供的一些参数的数目可以有变化的函数。例如我们很熟悉的printf,它需要有一个格式串,还应根据需要为它提供任意多个“其他参数”。这种函数被称作“具有变长度参数表的函数”,或简称为“变参数函数”。我们写程序中有时也可能需要定义这种函数。要定义这类函数,就必须使用标准头文件<stdarg.h>,使用该文件提供的一套机制,并需要按照规定的定义方式工作。本节介绍这个头文件提供的有关功能,它们的意义和使用,并用例子说明这类函数的定义方法。
C中变长实参头文件stdarg.h提供了一个数据类型va-list和三个宏(va-start、va-arg和va-end),用它们在被调用函数不知道参数个数和类型时对可变参数表进行测试,从而为访问可变参数提供了方便且有效的方法。va-list是一个char类型的指针,当被调用函数使用一个可变参数时,它声明一个类型为va-list的变量,该变量用来指向va-arg和va-end所需信息的位置。下面给出va_list在C中的源码:
- typedef char * va_list;
void va-start(va-list ap,lastfix)是一个宏,它使va-list类型变量ap指向被传递给函数的可变参数表中的第一个参数,在第一次调用va-arg和va-end之前,必须首先调用该宏。va-start的第二个参数lastfix是传递给被调用函数的最后一个固定参数的标识符。va-start使ap只指向lastfix之外的可变参数表中的第一个参数,很明显它先得到第一个参数内存地址,然后又加上这个参数的内存大小,就是下个参数的内存地址了。下面给出va_start在C中的源码:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //得到可变参数中第一个参数的首地址
_INTSIZEOF(n) 求n所占的字节数。如n为int,输出为:
111 & ~3 =4
输入3 :结果也为4
type va-arg(va-list ap,type)也是一个宏,其使用有双重目的,第一个是返回ap所指对象的值,第二个是修改参数指针ap使其增加以指向表中下一个参数。va-arg的第二个参数提供了修改参数指针所必需的信息。在第一次使用va-arg时,它返回可变参数表中的第一个参数,后续的调用都返回表中的下一个参数,下面给出va_arg在C中的源码:
#define va_arg(ap,type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) ) //将参数转换成需要的类型,并使ap指向下一个参数
在使用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访问函数调用的各个实际参数了。宏va_arg的类型特征可以大致地描述为:
类型 va_arg(va_list vap, 类型名)
在调用宏va_arg时必须提供有关实参的实际类型,这一类型也将成为这个宏调用的返回值类型。对va_arg的调用不仅返回了一个实际参数的值(“当前”实际参数的值),同时还完成了某种更新操作,使对这个宏va_arg的下次调用能得到下一个实际参数。对于我们的例子,其中对宏va_arg的一次调用应当写为:
v = va_arg(vap, int);
这里假定v是一个有定义的int类型变量。
在变参数函数的定义里,函数退出之前必须做一次结束动作。这个动作通过对局部的va_list变量调用宏va_end完成。这个宏的类型特征大致是:
void va_end(va_list vap);
三、栈中参数分布以及宏使用后的指针变化说明如下:
#include<iostream> using namespace std; #include<stdarg.h> int sum(int n,...) { int i , sum = 0; va_list vap; va_start(vap , n); //指向可变参数表中的第一个参数 for(i = 0 ; i < n ; ++i) sum += va_arg(vap , int); //取出可变参数表中的参数,并修改参数指针vap使其增加以指向表中下一个参数 va_end(vap); //把指针vap赋值为0 return sum; } int main(void) { int m = sum(3 , 45 , 89 , 72); cout<<m<<endl; return 0; }
这里首先定义了va_list变量vap,而后对它初始化。循环中通过va_arg取得顺序的各个实参的值,并将它们加入总和。最后调用va_end结束。
下面是调用这个函数的几个例子:
k = sum(3, 5+8, 7, 26*4);
m = sum(4, k, k*(k-15), 27, (k*k)/30);
函数sum中首先定义了可变参数表指针vap,而后通过va_start ( vap, n )取得了参数表首地址(赋值给了vap),其后的for循环则用来遍历可变参数表。这种遍历方式与我们在数据结构教材中经常看到的遍历方式是类似的。
函数sum看起来简洁明了,但是实际上printf的实现却远比这复杂。sum函数之所以看起来简单,是因为:
1、sum函数可变参数表的长度是已知的,通过num参数传入;
2、sum函数可变参数表中参数的类型是已知的,都为int型。
而printf函数则没有这么幸运。首先,printf函数可变参数的个数不能轻易的得到,而可变参数的类型也不是固定的,需由格式字符串进行识别(由%f、%d、%s等确定),因此则涉及到可变参数表的更复杂应用。
在这个函数中,需通过对传入的格式字符串(首地址为lpStr)进行识别来获知可变参数个数及各个可变参数的类型,具体实现体现在for循环中。譬如,在识别为%d后,做的是va_arg ( vap, int ),而获知为%l和%lf后则进行的是va_arg ( vap, long )、va_arg ( vap, double )。格式字符串识别完成后,可变参数也就处理完了。
在编写和使用具有可变数目参数的函数时,有几个问题值得注意。
第一:调用va_arg将更新被操作的va_list变量(如在上例的vap),使下次调用可以得到下一个参数。在执行这个操作时,va_arg并不知道实际有几个参数,也不知道参数的实际类型,它只是按给定的类型完成工作。因此,写程序的人应在变参数函数的定义里注意控制对实际参数的处理过程。上例通过参数n提供了参数个数的信息,就是为了控制循环。标准库函数printf根据格式串中的转换描述的数目确定实际参数的个数。如果这方面信息有误,函数执行中就可能出现严重问题。编译程序无法检查这里的数据一致性问题,需要写程序的人自己负责。在前面章节里,我们一直强调对printf等函数调用时,要注意格式串与其他参数个数之间一致性,其原因就在这里。
第二:编译系统无法对变参数函数中由三个圆点代表的那些实际参数做类型检查,因为函数的头部没有给出这些参数的类型信息。因此编译处理中既不会生成必要的类型转换,也不会提供类型错误信息。考虑标准库函数printf,在调用这个函数时,不但实际参数个数可能变化,各参数的类型也可能不同,因此不可能有统一方式来描述它们的类型。对于这种参数,C语言的处理方式就是不做类型检查,要求写程序的人保证函数调用的正确性。
假设我们写出下面的函数调用:
k = sum(6, 2.4, 4, 5.72, 6, 2);
编译程序不会发现这里参数类型不对,需要做类型转换,所有实参都将直接传给函数。函数里也会按照内部定义的方式把参数都当作整数使用。编译程序也不会发现参数个数与6不符。这一调用的结果完全由编译程序和执行环境决定,得到的结果肯定不会是正确的。
四、简单的练习
问题1:可变长参数的获取
有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:
va_arg (argp, float);
这样做可以吗?
答案与分析:
不可以。在可变长参数中,应用的是"加宽"原则。也就是float类型被扩展成double;char、 short类型被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对char和short类型的则用va_arg(argp, int)。