VA函数(variable argument function),参数个数可变函数,又称可变参数函数。
在C語言中,C標準函式庫的stdarg.h標頭檔定義了提供可變參數函數使用的巨集。在C++,應該使用標頭檔cstdarg。
要創建一個可變參數函數,必須把省略號(...)放到參數列表後面。函數內部必須定義一個va_list變數。然後使用巨集va_start、va_arg和va_end來讀取。例如:
#include <stdarg.h> double average(int count, ...) { va_list ap; int j; double tot = 0; va_start(ap, count); //使va_list指向起始的參數 for(j=0; j<count; j++) tot+=va_arg(ap, double); //檢索參數,必須按需要指定類型 va_end(ap); //釋放va_list return tot/count; }
可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数。如果在C++里,我们应该利用C++的多态性来实现可变参数的功能,尽量避免用C语言的方式来实现。
由于在C语言中没有函数重载,解决不定数目函数参数问题变得比较麻烦,即使采用C++,如果参数个数不能确定,也很难采用函数重载。对这种情况,提出了指针参数来解决问题。
// printf()函数,其原型为: int printf( const char* format, ...); //它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的,例如我们可以有以下不同的调用方法: printf( "%d ",i); printf( "%s ",s); printf( "the number is %d ,string is:%s ", i, s);我们需要以下几个宏定义:
#include <stdarg.h> int main(int argc,char *argv[]) { simple_va_fun(100); simple_va_fun(100,200); simple_va_fun(100,200,'a'); return 0; } void simple_va_fun(int i,...) { va_list arg_ptr; //定义可变参数指针 va_start(arg_ptr,i); // i为最后一个固定参数 int j=va_arg(arg_ptr,int); //返回第一个可变参数,类型为int char c=va_arg(arg_ptr,char); //返回第二个可变参数,类型为char va_end(arg_ptr); // 清空参数指针 printf( "%d %d %c\n",i,j,c); return; }
C函数调用的栈结构:
可变参数函数的实现与函数调用的栈结构密切相关,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。例如,对于函数:
void fun(int a, int b, int c) { int d; ... }其栈结构为
先看看固定参数列表函数:
void fixed_args_func(int a, double b, char *c) { printf("a = 0x%p\n", &a); printf("b = 0x%p\n", &b); printf("c = 0x%p\n", &c); }对于固定参数列表的函数,每个参数的名称、类型都是直接可见的,他们的地址也都是可以直接得到的,比如:通过&a我们可以得到a的地址,并通过函数原型声明了解到a是int类型的。
void var_args_func(const char * fmt, ...) { ... ... }这里我们只能得到fmt这固定参数的地址,仅从函数原型我们是无法确定"..."中有几个参数、参数都是什么类型的。回想一下函数传参的过程,无论"..."中有多少个参数、每个参数是什么类型的,它们都和固定参数的传参过程是一样的,简单来讲都是栈操作,而栈这个东西对我们是开放的。这样一来,一旦我们知道某函数帧的栈上的一个固定参数的位置,我们完全有可能推导出其他变长参数的位置。
我们先用上面的那个fixed_args_func函数确定一下入栈顺序。
int main() { fixed_args_func(17, 5.40, "hello world"); return 0; } a = 0x0022FF50 b = 0x0022FF54 c = 0x0022FF5C从这个结果来看,显然参数是从右到左,逐一压入栈中的(栈的延伸方向是从高地址到低地址,栈底的占领着最高内存地址,先入栈的参数,其地理位置也就最高了)。
c.addr = b.addr + x_sizeof(b); /*注意: x_sizeof !=sizeof */ b.addr = a.addr + x_sizeof(a);有了以上的"等式",我们似乎可以推导出 void var_args_func(const char * fmt, ... ) 函数中,可变参数的位置了。起码第一个可变参数的位置应该是:first_vararg.addr = fmt.addr + x_sizeof(fmt); 根据这一结论我们试着实现一个支持可变参数的函数:
#include <stdarg.h> #include <stdio.h> void var_args_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() { var_args_func("%d %d %s\n", 4, 5, "hello world"); return 0; }期待输出结果:
一切似乎很完美,编译也很顺利通过,但运行上面的代码后,不但得不到预期的结果,反而整个编译器会强行关闭(大家可以尝试着运行一下),原来是ap指针在后来并没有按照预期的要求指向第二个变参数,即并没有指向5所在的首地址,而是指向了未知内存区域,所以编译器会强行关闭。其实错误开始于:ap = ap + sizeof(int);由于内存对齐,编译器在栈上压入参数时,不是一个紧挨着另一个的,编译器会根据变参的类型将其放到满足类型对齐的地址上的,这样栈上参数之间实际上可能会是有空隙的。(参考(原文)C语言内存对齐相关文章)所以此时的ap计算应该改为:ap = (char *)ap +sizeof(int) + __va_rounded_size(int);
#include<stdio.h> #define __va_rounded_size(TYPE) \ (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int)) void var_args_func(const char * fmt, ...) { char *ap; ap = ((char*)&fmt) + sizeof(fmt); printf("%d\n", *(int*)ap); ap = (char *)ap + sizeof(int) + __va_rounded_size(int); printf("%d\n", *(int*)ap); ap = ap + sizeof(int) + __va_rounded_size(int); printf("%s\n", *((char**)ap)); } int main() { var_args_func("%d %d %s\n", 4, 5, "hello world"); return 0; }var_args_func只是为了演示,并未根据fmt消息中的格式字符串来判断变参的个数和类型,而是直接在实现中写死了。
#include <stdarg.h>#include <stdio.h> void std_vararg_func(const char *fmt, ...) { va_list ap; va_start(ap, fmt); printf("%d\n", va_arg(ap, int)); printf("%f\n", va_arg(ap, double)); printf("%s\n", va_arg(ap, char*)); va_end(ap); } int main() { std_vararg_func("%d %f %s\n", 4, 5.4, "hello world"); return 0;}对比一下 std_vararg_func和var_args_func的实现, va_list似乎就是char*, va_start似乎就是 ((char*)&fmt) + sizeof(fmt),va_arg似乎就是得到下一个参数的首地址。没错,多数平台下stdarg.h中va_list, va_start和var_arg的实现就是类似这样的。一般stdarg.h会包含很多宏,看起来比较复杂。
#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://www.jb51.net/article/41868.htm 《C/C++中可变参数的详细介绍》
--http://www.cnblogs.com/cpoint/p/3368993.html 《C语言中可变参数函数实现原理》