可变参数函数实现
va_list,va_start,va_arg,va_end
va可变参数意思,variable-argument。
1. 头文件及实现
linux中定义在gcc头文件中,stdarg.h中。
41 #ifndef __GNUC_VA_LIST 42 #define __GNUC_VA_LIST 43 typedef __builtin_va_list __gnuc_va_list; 44 #endif 45 46 /* Define the standard macros for the user, 47 if this invocation was from the user program. */ 48 #ifdef _STDARG_H 49 50 #define va_start(v,l) __builtin_va_start(v,l) 51 #define va_end(v) __builtin_va_end(v) 52 #define va_arg(v,l) __builtin_va_arg(v,l)
加builtin前缀的都是编译器内置函数,在机器上找不到源代码。
GCC provides a large number of built-in functions other than the ones mentioned above. Some of these are for internal use in the processing of exceptions or variable-length argument lists and are not documented here because they may change from time to time; we do not recommend general use of these functions.
The remaining functions are provided for optimization purposes.
GCC includes built-in versions of many of the functions in the standard C library. These functions come in two forms: one whose names start with the __builtin_
prefix, and the other without. Both forms have the same type (including prototype), the same address (when their address is taken), and the same meaning as the C library functions even if you specify the -fno-builtin option see C Dialect Options). Many of these functions are only optimized in certain cases; if they are not optimized in a particular case, a call to the library function is emitted.
在VS2008上的一种实现:
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 )
其实际就是利用函数参数入栈的顺序来计算参数地址,从而获知参数值。
函数参数是以数据结构:栈的形式存取,从右至左入栈。
首先是参数的内存存放格式:参数存放在内存的堆栈段中,在执行函数的时候,从最后一个开始入栈。因此栈底高地址,栈顶低地址,举个例子如下:
void func(int x, float y, char z);
那么,调用函数的时候,实参 char z 先进栈,然后是 float y,最后是 int x,因此在内存中变量的存放次序是 x->y->z,因此,从理论上说,我们只要探测到任意一个变量的地址,并且知道其他变量的类型,通过指针移位运算,则总可以顺藤摸瓜找到其他的输入变量。
2. 应用
一种实现可查到的实现:
typedef char* va_list; void va_start ( va_list ap, prev_param ); /* ANSI version */ type va_arg ( va_list ap, type ); void va_end ( va_list ap );
va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。
<Step 1> 在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);
<Step 2> 然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
<Step 3> 然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
<Step 4> 获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。说白了,就是让我们的程序具有健壮性。通常va_start和va_end是成对出现。
3. 使用总结
1)标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。
2)在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如
⑴在固定参数中设标志-- printf函数就是用这个办法。
⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法.
无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。
3)实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
①函数栈的生长方向
②参数的入栈顺序
③CPU的对齐方式
④内存地址的表达方式
结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现,将不再使用的指针设为NULL,这样可以防止以后的误操作。
4)取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。
可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数.如果在C++里,我们应该利用C++的多态性来实现可变参数的功能,尽量避免用C语言的方式来实现.
4. 一个示例
对printf一层封装:
void kprintf(int log_level, const char *format, …) { va_list ap; if(log_level <= current_log_level) { if(log_level == KDUMMY) return; va_start(ap, format); vprintf(format, ap); va_end(ap); } }
注:vprintf以va_list为参数打印输出到标准输出。
The functions printf() and vprintf() write output to stdout, the standard output stream。
int vprintf(const char *format, va_list ap);
参考:
- http://www.cnblogs.com/justinzhang/archive/2011/09/29/2195969.html
- http://www.cnblogs.com/hanyonglu/archive/2011/05/07/2039916.html
- http://blog.csdn.net/edonlii/article/details/8497704