本文主要是介绍C\C++ 中 va_start va_arg va_end 的使用和原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
◎用法:
func( Type para1, Type para2, Type para3, ... )
{
/****** Step 1 ******/
va_list ap;
va_start( ap, para3 ); //一定要“...”之前的那个参数
/****** Step 2 ******/
//此时ap指向第一个可变参数
//调用va_arg取得里面的值
Type xx = va_arg( ap, Type );
//Type一定要相同,如:
//char *p = va_arg( ap, char *);
//int i = va_arg( ap, int );
//如果有多个参数继续调用va_arg
/****** Step 3 ******/
va_end(ap); //For robust!
}
◎研究:
typedef char * va_list;
#define va_start _crt_va_start
#define va_arg _crt_va_arg
#define va_end _crt_va_end
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap) ( ap = (va_list)0 )
va_list argptr;
C语言的函数是从右向左压入堆栈的,调用va_start后,
按定义的宏运算,_ADDRESSOF得到v所在的地址,然后这个
地址加上v的大小,则使ap指向第一个可变参数如图:
栈底 高地址
| .......
| 函数返回地址
| .......
| 函数最后一个参数
| ....
| 函数第一个可变参数 <--va_start后ap指向
| 函数最后一个固定参数
| 函数第一个固定参数
栈顶 低地址
然后,用va_arg()取得类型t的可变参数值, 先是让ap指向下一个参数:
ap += _INTSIZEOF(t),然后在减去_INTSIZEOF(t),使得表达式结果为
ap之前的值,即当前需要得到的参数的地址,强制转换成指向此参数的
类型的指针,然后用*取值
最后,用va_end(ap),给ap初始化,保持健壮性。
example:(chenguiming)
#include <stdio.h>
#include <ctype.h>
#include<stdlib.h>
#include <stdarg.h>
int average( int first, ... ) //变参数函数,C++里也有
{
int count=0,i=first,sum=0;
va_list maker; //va_list 类型数据可以保存函数的所有参数,做为一个列表一样保存
va_start(maker,first); //设置列表的起始位置
while(i!=-1)
{
sum+=i;
count++;
i=va_arg(maker,int);//返回maker列表的当前值,并指向列表的下一个位置
}
return sum/count;
}
void main(void)
{
printf( "Average is: %d\n", average( 2, 3, 4,4, -1 ) );
}
//再贴上一个我的实例:
#include "stdafx.h"
#include <stdarg.h>
#include <iostream>
using namespace std;
void _tmain(int argc, _TCHAR* argv[],_TCHAR* envp[])
{
double AverageSalary(int,...);
cout<<"员工平均薪金: "<<AverageSalary(5,1234.56,1111.11,5500.00,2345.67,2222.22)<<"$"<<endl;
system("PAUSE");
}
double AverageSalary(int EmployeeTotal,...)
{
double SalaryTotal=0.0;
double Salary=0.0;
int n=EmployeeTotal;
va_list ap;
va_start(ap,EmployeeTotal);
while(n--)
{
Salary=va_arg(ap,double);
SalaryTotal+=Salary;
}
va_end(ap);
return(SalaryTotal?(SalaryTotal/EmployeeTotal):0);
}
//运行结果
员工平均薪金: 2482.71$
请按任意键继续. . .
逻辑很简单,首先定义
va_list marker;
表示参数列表,然后调用va_start()初始化参数列表。注意va_start()调用时不仅使用了marker
这个参数列表变量,还使用了first这个参数,说明参数列表的初始化与函数给定的第一个
确定参数是有关系的,这一点很关键,后续分析会看到原因。
调用va_start()初始化后,即可调用va_arg()函数访问每一个参数列表中的参数了。注意va_arg()
的第二个参数指定了返回值的类型(int)。
当程序确定所有参数访问结束后,调用va_end()函数结束参数列表访问。
这样看起来,访问变个数参数是很容易的,也就是使用va_list,va_start(),va_arg(),va_end()
这样一个类型与三个函数。但是对于函数变个数参数的机制,感觉仍是一头雾水。看来需要
继续深入探究,才能的到确切的答案了。
找到va_list,va_start(),va_arg(),va_end()的定义,在..."VC98"include"stdarg.h文件中。
.h中代码如下(只摘录了ANSI兼容部分的代码,UNIX等其他系统实现略有不同,感兴趣的朋友可以
自己研究):
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 )
从代码可以看出,va_list只是一个类型转义,其实就是定义成char*类型的指针了,这样就是为了
以字节为单位访问内存。
其他三个函数其实只是三个宏定义,且慢,我们先看夹在中间的这个宏定义_INTSIZEOF:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
这个宏的功能是对给定变量或者类型n,计算其按整型字节长度进行字节对齐后的长度(size)。在32位系统中
int占4个字节,16位系统中占2字节。
表达式
(sizeof(n) + sizeof(int) - 1)
的作用是,如果sizeof(n)小于sizeof(int),则计算后
的结果数值,会比sizeof(n)的值在二进制上向左进一位。
如:sizeof(short) + sizeof(n) - 1 = 5
5的二进制是0x00000101,sizeof(short)的二进制是0x00000010,所以5的二进制值比2的二进制值
向左高一位。
表达式
~(sizeof(int) - 1)
的作用时生成一个蒙版(mask),以便舍去前面那个计算值的"零头"部分。
如上例,~(sizeof(int) - 1) = 0x00000011(谢谢glietboys的提醒,此处应该是0xFFFFFF00)
同5的二进制0x00000101做"与"运算得到的是0x00000100,也就是4,而直接计算sizeof(short)应该得到2。
这样通过_INTSIZEOF(short)这样的表达式,就可以得到按照整型字节长度对齐的其他类型字节长度。
之所以采用int类型的字节长度进行对齐,是因为C/C++中的指针变量其实就是整型数值,长度与int相同,
而指针的偏移量是后面的三个宏进行运算时所需要的。
关于编程中字节对齐的内容请有兴趣的朋友到网上参考其他文章,这里不再赘述。
继续,下面这个三个宏定义:
第一:
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
编程中这样使用
va_list marker;
va_start( marker, first );
可以看出va_start宏的作用是使给定的参数列表指针(marker),根据第一个确定参数(first)所属类型的
指针长度向后偏移相应位置,计算这个偏移的时候就用到了前面的_INTSIZEOF(n)宏。
第二:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
此处乍一看有点费解,(ap += _INTSIZEOF(t)) - _INTSIZEOF(t)表达式的一加一减,对返回值是不起作用
的啊,也就是返回值都是ap的值,什么原因呢?
原来这个计算返回值是一方面,另一方面,请记住,va_start(),va_arg(),va_end这三个宏的调用是有关联
性的,ap这个变量是调用va_start()时给定的参数列表指针,所以
(ap += _INTSIZEOF(t)) - _INTSIZEOF(t)
表达式不仅仅是为了返回当前指向的参数的地址,还是为了让ap指向下一个参数(注意ap跳向下一参数是,
是按照类型t的_INTSIZEOF长度进行计算的)。
第三:
#define va_end(ap) ( ap = (va_list)0 )
这个很好理解了,不过是将ap指针置为空,算作参数读取结束。
至此,C/C++变个数函数参数的机制已经很清晰了。最后还要说一点要注意的问题:
在用va_arg()顺序跳转指针读取参数的过程中,并没有方法去判断所得到的下一个指针是否是有效地址,也
没有地方能够明确得知到底要读取多少个参数,这就是这种变个数参数的危险所在。前面的求平均数的例子
中,要求输入者必须在参数列表最后提供一个特殊值(-1)来表示参数列表结束,所以可以假设,万一调用
者没有遵循这种规则,将导致指针访问越界。
那么,可能有朋友会问,printf()函数就没有提供这样的特殊值进行标识啊。
别急,printf()使用的是另一种参数个数识别方式,可能比较隐蔽。注意他的第一个确定参数,也就是被我
们用作格式控制的format字符串,他的里面有"%d","%s"这样的参数描述符,printf()函数在解析format字符
串时,可以根据参数描述符的个数,确定需要读取后面几个参数。我们不妨做下面这样的试验:
printf("%d,%d,%d,%d"n",1,2,3,4,5);
实际提供的参数多于前面给定的参数描述符,这样执行的结果是
1,2,3,4
也就是printf()根据format字符串认为后面只有4个参数,其他的就不管了。那么再做一个试验:
printf("%d,%d,%d,%d"n",1,2,3);
实际提供的参数少于给定的参数描述符,这样执行的结果是(如果没有异常的话)
1,2,3,2367460
这个地方,每个人的执行结果可能都不相同,原因是读取最后一个参数的指针已经指向了非法的地址。这也是
使用printf()这类函数需要特别注意的地方。
说明:(sizeof(n)+sizeof(int)-1)&(~(sizeof(int)-1))的作用:
~是位取反的意思。
_INTSIZEOF(n)整个做的事情就是将n的长度化为int长度的整数倍。
比如n为5,二进制就是101b,int长度为4,二进制为100b,那么n化为int长度的整数倍就应该为8。
~(sizeof(int) - 1) )就应该为~(4-1)=~(00000011b)=11111100b,这样任何数& ~(sizeof(int) - 1) )后最后两位肯定为0,就肯定是4的整数倍了。
(sizeof(n) + sizeof(int) - 1)就是将大于4m但小于等于4(m+1)的数提高到大于等于4(m+1)但小于4(m+2),这样再& ~(sizeof(int) - 1) )后就正好将原长度补齐到4的倍数了。
这篇关于C\C++ 中 va_start va_arg va_end 的使用和原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!