一、从printf()开始
原型:int printf(const char * format, ...);
参数:format表示如何来格式字符串的指令,
…表示可选参数,调用时传递给"..."的参数可有可无,根据实际情况而定。
系统提供了vprintf系列格式化字符串的函数,用于编程人员封装自己的I/O函数。
int vprintf / vscanf(const char * format, va_list ap);
// 从标准输入/输出格式化字符串
int vfprintf / vfsacanf(FILE * stream, const char * format, va_list ap);
// 从文件流
int vsprintf / vsscanf(char * s, const char * format, va_list ap);
// 从字符串
// 例1:格式化到一个文件流,可用于日志文件
二、va函数的定义和va宏
C语言支持va函数,作为C语言的扩展--C++同样支持va函数,但在C++中并不推荐使用,C++引入的多态性同样可以实现参数个数可变的函数。不过,C++的重载功能毕竟只能是有限多个可以预见的参数个数。比较而言,C中的va函数则可以定义无穷多个相当于C++的重载函数,这方面C++是无能为力的。va函数的优势表现在使用的方便性和易用性上,可以使代码更简洁。C编译器为了统一在不同的硬件架构、硬件平台上的实现,和增加代码的可移植性,提供了一系列宏来屏蔽硬件环境不同带来的差异。
ANSI C标准下,va的宏定义在stdarg.h中,它们有:va_list,va_start(),va_arg(),va_end()。
简单地说,va函数的实现就是对参数指针的使用和控制。
typedef char * va_list; // x86平台下va_list的定义
函数的固定参数部分,可以直接从函数定义时的参数名获得;对于可选参数部分,先将指针指向第一个可选参数,然后依次后移指针,根据与结束标志的比较来判断是否已经获得全部参数。因此,va函数中结束标志必须事先约定好,否则,指针会指向无效的内存地址,导致出错。
这里,移动指针使其指向下一个参数,那么移动指针时的偏移量是多少呢,没有具体答案,因为这里涉及到内存对齐(alignment)问题,内存对齐跟具体使用的硬件平台有密切关系,比如大家熟知的32位x86平台规定所有的变量地址必须是4的倍数(sizeof(int) = 4)。va机制中用宏_INTSIZEOF(n)来解决这个问题,没有这些宏,va的可移植性无从谈起。
首先介绍宏_INTSIZEOF(n),它求出变量占用内存空间的大小,是va的实现的基础。
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
//第一个可选参数地址
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
//下一个参数地址
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
// 将指针置为无效
#define va_end(ap) ( ap=va_list0 )
1.va_arg身兼二职:返回当前参数,并使参数指针指向下一个参数。
初看va_arg宏定义很别扭,如果把它拆成两个语句,可以很清楚地看出它完成的两个职责。
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //下一个参数地址
// 将( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )拆成:
/* 指针ap指向下一个参数的地址 */
1). ap += _INTSIZEOF(t); // 当前,ap已经指向下一个参数了
/* ap减去当前参数的大小得到当前参数的地址,再强制类型转换后返回它的值 */
2). return *(t *)( ap - _INTSIZEOF(t))
回想到printf/scanf系列函数的%d %s之类的格式化指令,我们不难理解这些它们的用途了- 明示参数强制转换的类型。
(注:printf/scanf没有使用va_xxx来实现,但原理是一致的。)
2.va_end很简单,仅仅是把指针作废而已。
#define va_end(ap) (ap = (va_list)0) // x86平台
三、写一个简单的可变参数的C函数
下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的C函数要在程序中用到以下这些宏:
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在这里是variable-argument(可变参数)的意思.
这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.下面我们写一个简单的可变参数的函数,该函数至少有一个整数参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.
我们可以在我们的头文件中这样声明我们的函数:
extern void simple_va_fun(int i, ...);
我们在程序中可以这样调用:
simple_va_fun(100);
simple_va_fun(100,200);
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.
2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.
3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型.
4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数.
如果我们用下面三种方法调用的话,都是合法的,但结果却不一样:
1)simple_va_fun(100);
结果是:100 -123456789(会变的值)
2)simple_va_fun(100,200);
结果是:100 200
3)simple_va_fun(100,200,300);
结果是:100 200
我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果
正确,但和我们函数最初的设计有冲突.
我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于
1)硬件平台的不同
2)编译器的不同,所以定义的宏也有所不同,
下面以VC++中stdarg.h里x86平台的宏定义摘录如下('/'号表示折行):
typedef char * va_list;
#define _INTSIZEOF(n) /
((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.
C语言的函数是从右向左压入堆栈的,图(1)是函数的参数在堆栈中的分布位置.
我们看到va_list被定义成char*,有一些平台或操作系统定义为void*.
再看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址,如图:
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n个参数(第一个可变参数) |
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<-- &v
图( 1 )
然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我们看一下va_arg取int型的返回值:
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址(图2).
然后用*取得这个地址的内容(参数值)赋给j.
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|<--va_arg后ap指向
|第n个参数(第一个可变参数) |
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<-- &v
图( 2 )
最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;
使ap不再指向堆栈,而是跟NULL一样.
有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.
在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.
关于va_start, va_arg, va_end的描述就是这些了,我们要注意的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.
四、可变参数在编程中要注意的问题
因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型.
有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数printf是从固定参数format字符串来分析出参数的类型,再调用va_arg的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.
另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.如果simple_va_fun()改为:
可变参数为char*型,当我们忘记用两个参数来调用该函数时,就会出现
core dump(Unix) 或者页面非法的错误(window平台).但也有可能不出
错,但错误却是难以发现,不利于我们写出高质量的程序.
以下提一下va系列宏的兼容性.
System V Unix把va_start定义为只有一个参数的宏:
va_start(va_list arg_ptr);
而ANSI C则定义为:
va_start(va_list arg_ptr, prev_param);
如果我们要用system V的定义,应该用vararg.h头文件中所定义的宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以用ANSI C的定义就够了,也便于程序的移植.
以下分析中的地址标示在不同的机器可能不一样。
先看一下可变参数的函数实例:
在Window平台(win32/Intel)
C/C++默认的__cdecl调用方式:从右向左压栈
根据上面的知识我们就可以很轻松的解释Test1()多参数函数调用的秘密。
main()调用Test1(3, 1, 2, 3), 汇编如下:
89: Test1(3, 1, 2, 3);
0040DB88 push 3
0040DB8A push 2
0040DB8C push 1
0040DB8E push 3
0040DB90 call @ILT+55(Test1) (0040103c)
0040DB95 add esp,10h
函数压栈后,可能的堆栈分布如下:
0X100C3
0X10082
0X10041
0X10003
在Test1(int num, ...)中
调用:
va_list argList;
va_start(argList, num);
得:argList ----> 0X1004
第一次调用:int var = va_arg(argList, int);
argList ----> 0X1008
var = 1
...
如此循环,从而获取所有的参数
在上述知识的前提下, 我们再来看如下例子:
得出的结果竟然是:5445018 ????
问题出在什么地方?
我们参考:va_start(ap, v)宏的定义:
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
当参数中包含对某个变量的引用时,ap获得地址将不是参数在堆栈中的地址,而是被传入
的变量的地址,上例中为cnt的地址。这个时候ap已经成为野指针,极有可能会导致极为
严重的bug产生。
可以做如下测试:
在main()中调用:
// 注意cnt的地址 = 0x0012ff7c
Test2_1(cnt, 1, 2, 3);
察看变量的值,可以知道:
参数first在堆栈中的地址为:pN1 = 0x0012ff18
argList的地址为:0x0012ff80 = 0x0012ff18 + sizeof(first)
遇到这种情况可以自行实现va_start(ap, v),可以参考
ms-help://MS.MSDNQTR.2003FEB.2052/enu_kbvisualc/en-us/visualc/Q119394.htm
给出的一种实现
#define va_start(ap,v) {int var= _INTSIZEOF(v); /
__asm lea eax,v __asm add eax,var __asm mov ap,eax /
}