可变参数给编程带来了很大的方便,在享受它带来的方便的同时,很有必要了解一下其实现方式,在了解编程语言的同时,也可以扩展编程的思路。
可变参数需要用到3个宏函数和一个类型,他们都定义在<stdarg.h>中,分别是:
va_start(vl)
va_arg(vl, type)
va_end(vl)
其中vl是va_list类型,type就是对象类型(如int, double或 自定义的struct之类的)。
va_start函数用来初始化vl
va_arg(vl, type)用来取得type类型的变量值,这个宏会不可逆的改变vl,所以调用va_arg是要有顺序,不能乱搞,具体顺序就是参数顺序。不可逆的意思就是只能按顺序调用一次。当然你可以再调用va_start,然后又按顺序遍历一次。
va_end就是使得vl无效。表示参数获取完毕
一个简单的例子:
#include <stdio.h> void VariableArgumentMethod(int argc, ...); int main(){ VariableArgumentMethod(6, 4, 7, 3, 0, 7, 9); return 0; } void VariableArgumentMethod(int argc, ...){ // 声明一个指针, 用于持有可变参数 va_list pArg; // 将 pArg 初始化为指向第一个参数 va_start(pArg, argc); // 输出参数 for(int i = 0; i != argc; ++i){ // 获取 pArg 所指向的参数并输出 printf("%d, ", va_arg(pArg, int) ); } va_end(pArg); }
可变参数的实现其实就是利用第一个参数的地址,以及其余参数的类型来确定他们的地址。或者说知道一个基地址(由第一个参数提供),以及一个参数的相对于基地址的偏移量(由参数类型提供),自然就这个参数的地址啦。
因此,可变函数有两个必要条件,1.第一个参数必须是显式提供的,这样才能知道参数在栈中的基地址。2.所有参数类型必须要知道。(printf的第一个参数format中的%d,%lf呀 的作用就是提供参数类型,以确定参数位置)
之后要理解这些宏,需要清楚调用函数时是如何传参的,传参顺序是什么,以及地址对齐。详见:
http://www.cnblogs.com/cpoint/p/3368993.html
简而言之,传参顺序在stdcalll下是从右往左入栈,栈底处于高地址,栈顶处于低地址,栈的增长方向是高到低。如func(arg1, arg2, arg3),它们地址是:&arg1 < &arg2 < &arg3并且连续。
(值得一提的是,我在gcc中查看固定参数的函数时,如func(arg1, arg2, arg3),它们的地址居然是&arg1 > &arg2 > &arg3。 后来查看汇编才知道原来编译器又用了个局部变量来存参数,printf打印的是局部变量的地址)
这里我只写一些我对这些宏的理解。
首先给出实现细节:
typedef char * va_list; #define _ADDRESSOF(v) ( &(v) ) #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) ) #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #define va_end(ap) ( ap = (va_list)0 )
_INTSIZEOF(n) 表示n这个类型是int类型的几倍(不是整数倍的向上取整)。比如说double类型是int的2倍,char类型是int的1倍,一个 sizeof(struct my_struct)=10的类型是Int类型的3倍。为什么要是int的整数倍,待会再说。
va_start(ap, v) ap为va_list类型,v为第一个参数。它的作用是将ap复制为第二个参数的起始地址。
va_arg(ap, t) ap为va_list类型,t为你想要获得的参数的类型。作用是先将返回ap值,再将ap设为下一个参数的起始地址。
va_end(ap) 设为空指针。
其中va_start和va_arg都调用了_INTSIZEOF(n), _INTSIZEOF(n)这个宏的作用其实就是确定n这个类型的实际占用的内存空间(也就是考虑了地址对齐)。为什么它必须是int的整数倍呢?按普通的对齐规则,char无论如何都只要1个字节的空间。然而在传参数的时候
却有例外,要4个字节。这是因为传参的时候还发生了类型提升。
在C语言中,调用一个不带原型声明的函数时,调用者会对每个参数执行“默认实际参数提升(default argument promotions)”。该规则同样适用于可变参数函数——对可变长参数列表超出最后一个有类型声明的形式参数之后的每一个实际参数,也将执行上述提升工作。
提升工作如下:
——float类型的实际参数将提升到double
——char、short和相应的signed、unsigned类型的实际参数提升到int
——如果int不能存储原值,则提升到unsigned int
因此下面的代码是错误的,运行时得不到预期的结果:
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);
弄明白了_INTSIZEOF含义后。继续解释va_start (ap, v) 它是令ap为第二个参数的起始地址。(v即提供了基地址,又提供了它所占的内存空间,而第二个参数紧跟在第一个参数后面,很自然就能得到第二个参数的位置)
va_arg(ap, t) 实际它和第一个宏原理是一样的。只是有点小变化,基地址ap已经由va_start得到了,t表示参数类型。
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏刚开始看肯定觉得有点奇怪。 先将ap+_INTSIZEOF(t), 再减去_INTSIZEOF(t)那么还是ap原来的值吗?呵呵,其实 关键点在于+= , 这个宏其实有个副作用,返回的值确实是原来的值没变,但是ap本身+_INTSIZEOF(t) 到了下一个参数的起始地址.
va_end(ap) 没什么好讲的。呵呵
最后附上一个printf简易版本:
#include<stdarg.h> void minprintf(char *fmt, ...) { va_list ap; char *p, *sval; int ival; double dval; va_start(ap, fmt); for (p = fmt; *p; p++) { if(*p != '%') { putchar(*p); continue; } switch(*++p) { case 'd': ival = va_arg(ap, int); printf("%d", ival); break; case 'f': dval = va_arg(ap, double); printf("%f", dval); break; case 's': for (sval = va_arg(ap, char *); *sval; sval++) putchar(*sval); break; default: putchar(*p); break; } } va_end(ap); }
参考链接:
http://blog.chinaunix.net/uid-27666459-id-3772622.html
http://www.cnblogs.com/cpoint/p/3368993.html
http://www.cnblogs.com/Anker/archive/2012/12/27/2836495.html