va_start,va_arg,va_end的使用

一、在C中,当我们无法列出传递函数的所有实参的类型和数目时,可以用省略号指定参数表。
void fun(...);
void fun(parm_list,...);
#include <stdio.h>
void fun(int n,...)
{
    int *temp=&n;
    temp++;
    for(int i=0;i<n;i++)
    {
        printf("%d\n",*temp);
        temp++;
    }
}
int main()
{
    int a=1,b=2,c=3,d=4;
    fun(4,a,b,c,d);
    return 0;
}

二、使用va_start,va_arg,va_end

#include <stdio.h>
#include <stdarg.h>
void fun(int n,...)    //变参函数,C++里也有
{
    int temp,i;
    va_list ap;     //step1:va_list类型数据可以保存函数的所有参数,做为一个列表一样保存
 
    va_start(ap,n);   //step2:对ap 进行初始化,让ap指向可变参数表里面的第一个参数(最后一个固定参数后的可变参数)
    for(i=0;i<n;i++)
    {
        temp=va_arg(ap,int);  //step3:把 ap 的位置指向变参表的下一个变量位置
        printf("%d\n",temp);
    }
    va_end(ap);   //step4:关闭ap指针
}
int main()
{
    int a=1,b=2,c=3,d=4;
    fun(4,a,b,c,d);
    return 0;
}

<Step 1> 在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);
 
<Step 2> 然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
 
<Step 3> 然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
 
<Step 4> 获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。说白了,就是让我们的程序具有健壮性。通常va_start和va_end是成对出现。
 
三、函数参数传递原理
这里要知道两个事情:
     ⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
     (2)在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。  
  栈底 高地址
    | .......     
    | 函数返回地址
    | .......      
    | 函数最后一个参数
    | ....                       
    | 函数第一个可变参数       <--va_start后ap指向  
    | 函数最后一个固定参数
    | 函数第一个固定参数  
    栈顶 低地址
所使用到的宏:
     void va_start( va_list arg_ptr, prev_param );
     type va_arg( va_list arg_ptr, type );
     void va_end( va_list arg_ptr );
宏解释:
    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 )
1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。
 
4、va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
因此,现在再来看va_arg()的实现就应该心中有数了:
     #define va_arg(ap,t)     ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏做了两个事情,
    ①ap指向下一个参数:ap += _INTSIZEOF(t)
    ②表达式取得本参数的值:先是让ap指向下一个参数:ap += _INTSIZEOF(t),然后在减去_INTSIZEOF(t),使得表达式结果为ap之前的值,即当前需要得到的参数的地址,强制转换成指向此参数的
类型的指针,然后用*取值
 
int main(void)
{
    int a=0,b=0;
    b=((a+=1)-1);
    printf("a:%d,b:%d\n",a,b);
    b=((a+=1)-1);
    printf("a:%d,b:%d",a,b);
    return 0;
}

5、va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.
 
posted @ 2014-08-24 21:06  Eliefly  阅读(1917)  评论(0编辑  收藏  举报