您的位置:首页 > 编程语言 > C语言/C++

C\C++ 中 va_start va_arg va_end 的使用和原理

2014-12-07 21:38 525 查看
◎用法:

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的倍数了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: