va_list在win32/vc++6.0下的讨论
1. 简介
va_list、va_arg、va_end是为了处理变参数的函数而做的宏定义,这些定义会因为平台(cpu、操作系统)和环境(编译系统)的不同而有所不同。
简单原理:编译系统编译时,会将函数的参数依次放到栈中,这样根据固定参数的地址以及固定参数给出的相关信息很容易得到可变参数的个数、类型、值。注意一点,这些或者是固定参数给出的信息,虽然不是直接给出的;或者是程序写作者自我约定。得到了可变参数,剩下的处理和普通函数一样了。
以下的理解和实例均在win32&vc++6.0环境中的情况。
2. 结合一个实例看简单原理
实例如下(win2kserver vc++6.0编译通过):
//============================
//求若干个整数的平均值
#include
#include
int AveInt(int,...);
void main()
{
printf("%d/t",AveInt(2,2,3));
printf("%d/t",AveInt(4,2,4,6,8));
return;
}
int AveInt(int num,...)
{
int ReturnValue=0;
int i=num;
va_list myvalist;
va_start(myvalist,num);
while(i>0)
{
ReturnValue+=va_arg(myvalist,int);
i--;
}
return ReturnValue/=num;
}
//===============================
(1) 分配情况:跟第二次调用AveInt(4,2,4,6,8),看下参数在内存中的分配,见下图。
图1
明显,参数依次连续分配;
(2) 参数的个数:由第一个参数给定;
(3) 参数类型:我约定为整数;
(4) 参数值:明显了。
3. 两个小知识(自我整理下)
(1) win32 vc++编译
内存分配由高址向低址进行;参数调用时,参数入栈顺序:从最后一个开始,直到第一个。通过上图可以看到这两点。
(2) 内存对齐
分为结构成员内存对齐和栈内存对齐。
前者要求:字、双字和四字在自然边界上不需要在内存中对齐。(对字、双字和四字来说,自然边界分别是偶数地址、可以被4整除的地址、和可以被8整除的地址。)一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为未对齐的。
后者要求:总保持对齐,而且对齐在4字节边界上。
两者区别:前者是可以通过工具控制对齐边界的,如在vc++中就可以通过控制选项控制边界;而后者是不能控制的,必须对齐的,毕竟栈的效率太影响到程序性能了。
4. 宏定义
以下是vc++6.0定义在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 )
结合上例,调用AveInt(4,2,4,6,8),当运行了va_start(myvalist,num);之后,根据定义myvalist =(va_list)&num+_INTSIZEOF(i),这样myvalist指向第一个可变参数2;第一次运行va_arg(myvalist,int);后,先让myvalist+= _INTSIZEOF(int)指向下一个可变参数4,然后在取出前面的可变参数2进行处理;依此类推。
以下是跟踪AveInt(4,2,4,6,8)的运行内存截图:
图2:va_start(myvalist,num);之后
图3:第一次运行va_arg(myvalist,int);后
图4:第二次运行va_arg(myvalist,int);后
图5:第三次运行va_arg(myvalist,int);后
5. 注意:易出错
由于是可变参数,对编译器来说,检查类型一般比较困难,所以,编译器对类型检查不严格,容易出现由于程序写作者的疏忽导致的错误。例如,可变参数中有个int型的,然而在函数中把这个int型参数当作char *进行处理,就可能导致内存越界等错误。