一般我们编程的时候,函数中形式参数的数目通常是确定的,在调用时要依次给出与形式参数对应的所有实际参数。但在某些情况下希望函数的参数个数可以根据需要确定,因此c语言引入可变参数函数。这也是c功能强大的一个方面,例如我们经常用到的标准输入scanf和标准输出printf函数,函数原型如下:
int scanf(const char *format, ...); int printf(const char *format, ...);
printf("%d",i);
printf("%s",s);
printf("the number is %d ,string is:%s", i, s);
1、处理方法#inlcude<stdarg.h>
void va_start( va_list arg_ptr, prev_param);
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr );
va_list:用来保存宏va_start、va_arg和va_end所需信息的一种类型。为了访问变长参数列表中的参数,必须声明va_list类型的一个对象。
va_start:访问变长参数列表中的参数之前使用的宏,它初始化用va_list声明的对象,初始化结果供宏va_arg和va_end使用;
va_arg:展开成一个表达式的宏,该表达式具有变长参数列表中下一个参数的值和类型。每次调用va_arg都会修改用va_list声明的对象,从而使该对象指向参数列表中的下一个参数;
va_end:该宏使程序能够从变长参数列表用宏va_start引用的函数中正常返回。
#include <stdio.h> #include <stdlib.h> #include <stdarg.h> void fun(char*fmt,...) { int m; double d; char *ptr; va_list ap; //定义一个va_list类型变量 va_start(ap,fmt); //获取第二个参数的地址 m = va_arg(ap,int); //第二个参数是int类型,获取值 d = va_arg(ap,double); //第三个参数是double类型,获取值 ptr = va_arg(ap,char*); //第四个参数是char*类型,获取值 va_end(ap); printf("%d\n",m); printf("%lf\n",d); printf("%s\n",ptr); } int main() { fun("%d %f %s\n", 4, 5.4, "hello world"); system("pause"); return 0; }
程序执行结果如下:
可变参类型陷井
下面的代码是错误的,运行时得不到预期的结果:
view plaincopy to clipboardprint?
va_start(pArg, plotNo);
fValue = va_arg(pArg, float); // 类型应改为double,不支持float
va_end(pArg);
va_start(pArg, plotNo);
fValue = va_arg(pArg, float); // 类型应改为double,不支持float
va_end(pArg);
下面列出va_arg(argp, 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
然后,调用者将提升后的参数传递给被调用者。所以,可变参函数内是绝对无法接收到上述类型的实际参数的。
关于该陷井,C/C++著作中有以下描述:在《C语言程序设计》对可变长参数列表的相关章节中,并没有提到这个陷阱。但是有提到默认实际参数提升的规则:在没有函数原型的情况下,char与short类型都将被转换为int类型,float类型将被转换为double类型。 ——《C语言程序设计》第2版 2.7 类型转换 p36
2、可变参数解析:
先来看看一个固定参数函数,对于固定参数列表的函数,每个参数的名称、类型都是直接可见的,他们的地址也都是可以直接得到的。例如下面的程序:
int func(int i,float f,char *s) { printf("i address is: 0X%p\n",&i); printf("f address is: 0X%p\n",&f); printf("s address is: 0X%p\n",&s); } int main() { func(23,45.5,"hello"); return 0; }
程序结果如下:
从结果可以看出函数参数是从右到左逐一压入栈中的(栈的延伸方向是从高地址到低地址,栈底的占领着最高内存地址,先入栈的参数,其地理位置也就最高了)。
如果不考虑内存对齐的话,可以推导出:&f = &i+sizeof(f) ,&s = &f+sizeof(s)。
有了以上的"等式",我们似乎可以推导出 void func(const char * fmt, ... ) 函数中,可变参数的位置了。程序如下:
int func(const char*fmt,...) { char *ap; ap = (char*)&fmt + sizeof(fmt); printf("%d\n",*(int*)ap); ap = ap+sizeof(int); printf("%d\n",*(int*)ap); ap = ap+sizeof(int); printf("%s\n",*(char**)ap); } int main() { func("%d %f %s\n", 60, 54, "hello world"); system("pause"); return 0; }
程序执行结果如下:
实际当中由于内存对齐,编译器在栈上压入参数时,不是一个紧挨着另一个的,编译器会根据变参的类型将其放到满足类型对齐的地址上的,这样栈上参数之间实际上可能会是有空隙的。上述例子中,都是4,正好对齐了。
看看这几个宏到底干了什么
typedef char * va_list; // 这个仅仅是个重定义
// 获取v的地址
#define _ADDRESSOF(v) ( &(v) )
// n的整数字节的大小,必须是sizeof(int)的整数倍。如sizeof(n)为5的话,_INTSIZEOF(n)为8(假设为32位机器的话)
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
// 给v的地址加上v的大小
#define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
// 给ap自增t的大小,并且获取原有ap的地址的数据,强制转型为t类型
// 这个相当于 ( *(t *)ap )
// (ap += _INTSIZEOF(t))
// 这一个宏相当于完成两件事情
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
// 给ap置0
#define va_end(ap) ( ap = (va_list)0 )
C函数的调用规则了,在调用一个函数之前,调用方会将这个函数参数push(修改ESP指针),并且push规则是先push最后一个参数,最后push第一个参数,因此ESP指针最后应该是指向第一个参数。可变参数就是利用了这一点,一旦获取到第一个参数的地址后,就能够通过地址向前查找所有的参数。(注意:x86上的堆栈是反向的,push会使ESP的值减少,而不是增加)。
实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
①函数栈的生长方向
②参数的入栈顺序
③CPU的对齐方式
④内存地址的表达方式
结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现—将不再使用的指针设为NULL,这样可以防止以后的误操作。