精华内容
下载资源
问答
  • 变长参数va_list va_start va_arg va_end

    千次阅读 2020-06-29 16:34:16
    这种变长参数,需要使用va_list va_start va_end va_arg来访问参数。 下面是一个tutorialspoint 的一个使用demo,示范如何使用这几个接口 #include<stdarg.h> #include<stdio.h> int sum(int num_args, ...

    对于int printf(const char *format, ...);这种变长参数,需要使用va_list va_start va_end va_arg来访问参数。
    下面是一个tutorialspoint 的一个使用demo,示范如何使用这几个接口

    #include<stdarg.h>
    #include<stdio.h>
    int sum(int num_args, ...) {
       int val = 0, i;
       va_list ap;
       va_start(ap, num_args);
       for(i = 0; i < num_args; i++) {
          val += va_arg(ap, int);
       }
       va_end(ap);
       return val;
    }
    int main(void) {
       printf("Sum of 10, 20 and 30 = %d\n",  sum(3, 10, 20, 30) );
       printf("Sum of 4, 20, 25 and 30 = %d\n",  sum(4, 4, 20, 25, 30) );
    
       return 0;
    }
    

    编译运行之后的输出:

    Sum of 10, 20 and 30 = 60
    Sum of 4, 20, 25 and 30 = 79
    

    从上面看,访问变长参数需要遵循特定的接口,步骤如下:

    • 在调用参数表之前,定义一个 va_list 类型的变量ap;
    • 通过 va_start对ap进行初始化,之后ap指向可变参数表里面的第一个参数
    • 通过va_arg遍历参数,需要知道参数的类型,上面demo的参数全部是int型
    • 调用va_end关闭ap,以免发生误使用

    而对于printf这类变长参数接口,没有num_args传递参数的个数,而且变长参数的类型也各不相同,又是如何访问他们的。在glibc printf实现中,需要根据format中限定词Conversion specifiers来决定如何遍历变长参数,例如printf("str:%s num:%d\n", "nihao", a);,处理时遍历根据format中,当发现转义%之后开始解析,对于s说明第一个变长参数是char *类型,对于d说明第二个变长参数是int类型,即format中隐含变长参数的个数和类型。

    实现

    接着分析一下背后的原理是如何能够访问变长参数的。

    reflink:
    https://zh.wikipedia.org/zh-hans/X86%E8%B0%83%E7%94%A8%E7%BA%A6%E5%AE%9A#cdecl
    https://www.codeproject.com/Articles/1388/Calling-Conventions-Demystified
    https://gcc.gnu.org/onlinedocs/gcc/x86-Function-Attributes.html

    下面是程序员的自我修养-编译和链接中的示例,并且是baidu出的最广泛的结果

          typedef char * va_list;     // TC中定义为void*
          #define _INTSIZEOF(n)    ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) //为了满足需要内存对齐的系统
          #define va_start(ap,v)    ( ap = (va_list)&v + _INTSIZEOF(v) )     //ap指向第一个变参的位置,即将第一个变参的地址赋予ap
          #define va_arg(ap,t)       ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )   /*获取变参的具体内容,t为变参的类型,如有多个参数,则通过移动ap的指针来获得变参的地址,从而获得内容*/
          #define va_end(ap) ( ap = (va_list)0 )   //清空va_list,即结束变参的获取
    

    不过这种只适用于早期的cdecl函数调用方式(不信的可以自己试试结果),在x86平台上更是只能在32bit上使用,在64bit上就算限定使用cdecl约定也会被直接忽略。

    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 )
    
    int __attribute__((__cdecl__)) sum(int num_args, ...) {	//使用__attribute__((__cdecl__))指定使用
    。。。
    }
    

    之后使用gcc -m32编译成32bit应用,这样可以得到我们期望的结果。

    不过目前的调用约定默认都是fastcall了,在x86_64上前6个参数通过rdi, rsi, rdx, rcx, r8, r9寄存器传递,剩余的参数放在栈上传递。

    所以变长参数的访问方式和上面cdecl就不再相同了,下面探索一下现代的fastcall中是如何实现访问变长参数的。

    仍然使用最开始的例子,通过gcc --save-temps保留中间文件,特别是.i文件,看到其中sum的展开,它不再是个宏

    int sum(int num_args, ...) {
       int val = 0;
       va_list ap; 
       int i;
    
       __builtin_va_start(ap,num_args);
       for(i = 0; i < num_args; i++) {
          val += __builtin_va_arg(ap,int);
       }
       __builtin_va_end(ap);                                                                                                                       
    
       return val;
    }
    

    发现很难找到__builtin_va_start,__builtin_va_end的实现,它和va_list是arch相关的,在gcc内部实现的gcc/builtins.c

    /usr/lib/gcc/x86_64-linux-gnu/5/include/stdarg.h
    #define va_start(v,l)   __builtin_va_start(v,l)                                    
    #define va_end(v)   __builtin_va_end(v)                                            
    #define va_arg(v,l) __builtin_va_arg(v,l)  
    

    在此不打算翻gcc的烂脚布了,后面我直接从汇编上分析这个过程,代码不多可以手动解析,不感兴趣的可以略过后面的汇编注释部分,直接看下面的文字描述

    1. 将所有上一帧的传参寄存器入栈保存到栈上addr处
    2. 构建va_list变量,struct __va_list_tag类型的,初始化其中的成员。reg_save_area指向寄存器传参保存的地址,当参数大于能够直接通过寄存器传递的数量时,保存到caller的栈上,overflow_arg_area指向caller中保存参数的位置。其中gp_offset代表addr中的偏移,fp_offset代表寄存器传参区域的虽大范围,x86_64上允许最多6个参数通过寄存器传参,而第一个rdi寄存器不可能传递的是变长参数,所以做多允许5个变长参数通过寄存器传递过来。
    3. 遍历变长参数列表,如果变长参数少于5个,则只需要访问reg_save_area区域,gp_offset代表偏移,每次遍历,gp_offset每次增加8个字节的偏移以指向下一个参数的地址。如果变长参数大于5个,则剩下的参数开始访问overflow_arg_area区域,每次遍历,overflow_arg_area地址增加8指向一个参数的地址。
      va_list

    究其背后,也就是一种函数调用约定的改变需要一种新的实现方式来访问变长参数,它只是约定的附属产物,如果你看不惯了,也可以自己实现一种,另一篇linux函数调用过程中的寄存器中已经说明了函数调用的约定,以及我们什么时候需要遵守这些约定。

    软件发展的这么快,我们不可能知道所有的知识,但是掌握其背后的原理能够帮助我们迅速掌握其他关联的知识。

    gdb main
    (gdb) ptype va_list	             //看一下va_list的类型,它不再是char *
    type = struct __va_list_tag {
        unsigned int gp_offset;
        unsigned int fp_offset;
        void *overflow_arg_area;
        void *reg_save_area;
    } [1]
    
    sum:
    .LFB1:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16 
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $92, %rsp
        movq    %rsi, -168(%rbp)		//寄存器保存到栈上,没有包括rdi寄存器,它不属于变长参数列表中
        movq    %rdx, -160(%rbp)
        movq    %rcx, -152(%rbp)
        movq    %r8, -144(%rbp)
        movq    %r9, -136(%rbp)
        testb   %al, %al 				//直接跳转到L4 label处
        je  .L4 
        movaps  %xmm0, -128(%rbp)	    //这里的浮点处理暂时不关心
        movaps  %xmm1, -112(%rbp)
        movaps  %xmm2, -96(%rbp)
        movaps  %xmm3, -80(%rbp)
        movaps  %xmm4, -64(%rbp)
        movaps  %xmm5, -48(%rbp)
        movaps  %xmm6, -32(%rbp)
        movaps  %xmm7, -16(%rbp)
    .L4:
        movl    %edi, -212(%rbp)    //num_arg保存到这里                                                                                                                  
        movl    $0, -180(%rbp)      //val = 0
        //va_start初始化
        movl    $8, -208(%rbp)      //gp_offset = 8,第一个寄存器肯定不能存储变长参数
        movl    $48, -204(%rbp)     //fp_offset = 48
        leaq    16(%rbp), %rax      //如果参数多于寄存器所能传的最大数量时,入栈保存。在rbp需要跨越16个字节,分别保存了上一个栈帧的rbp和返回地址
        movq    %rax, -200(%rbp)    //overflow_arg_area=上一个栈帧中可能保存参数的位置
        leaq    -176(%rbp), %rax    //参数寄存器保存到栈上的位置,并且加上了一个rdi寄存器的位置
        movq    %rax, -192(%rbp)    //reg_save_area=寄存器参数保存的位置
        movl    $0, -184(%rbp)		//i = 0
        jmp .L5
    .L8:
        movl    -208(%rbp), %eax
        cmpl    $48, %eax			//比较gp_offset和fp_offset,即最多支持5((48-8)/8)个直接参数
        jnb .L6                     //如果变长参数大于6个,到overflow_arg_area中取参数
        movq    -192(%rbp), %rdx    //根据gp_offset和reg_save_area找到参数的地址
        movl    -208(%rbp), %eax
        movl    %eax, %eax			//看不懂O(∩_∩)O~
        addq    %rdx, %rax
        movl    -208(%rbp), %edx
        addl    $8, %edx			//gp_offset += 8,更新gp_offset
        movl    %edx, -208(%rbp)    
        jmp .L7
    .L6:
        movq    -200(%rbp), %rdx	//剩余参数的获取
        movq    %rdx, %rax
        addq    $8, %rdx
        movq    %rdx, -200(%rbp)   //随着遍历参数,更新overflow_arg_area地址
    .L7:
        movl    (%rax), %eax
        addl    %eax, -180(%rbp)	//val+=va_arg(ap, int); 加法
        addl    $1, -184(%rbp)		//i++
    .L5:
        movl    -184(%rbp), %eax   //eax = i
        cmpl    -212(%rbp), %eax   //i < num_args
        jl  .L8
        movl    -180(%rbp), %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc   
    
    展开全文
  • 写可变参数的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(可变...
  • typedef char *va_list; va_start宏,获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数最左边的参数): #define va_start(list,param1) ( list = (va_list)&amp;param1+ ...

    在ANSI C中,这些宏的定义位于stdarg.h中:

    typedef char *va_list;

    va_start宏,获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数最左边的参数):

    #define va_start(list,param1)   ( list = (va_list)&param1+ sizeof(param1) )

    va_arg宏,获取可变参数的当前参数,返回指定类型并将指针指向下一参数(mode参数描述了当前参数的类型):

    #define va_arg(list,mode)   ( (mode *) ( list += sizeof(mode) ) )[-1]

    va_end宏,清空va_list可变参数列表:

    #define va_end(list) ( list = (va_list)0 )

    注:以上sizeof()只是为了说明工作原理,实际实现中,增加的字节数需保证为为int的整数倍

    如:#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

    为了理解这些宏的作用,我们必须先搞清楚:C语言中函数参数的内存布局。首先,函数参数是存储在栈中的,函数参数从右往左依次入栈。

    以下面函数为讨论对象:

    void test(char *para1,char *param2,char *param3, char *param4) { va_list list; ...... return; }

    在linux中,栈由高地址往低地址生长,调用test函数时,其参数入栈情况如下:

     当调用va_start(list,param1) 时:list指针指向情况对应下图:

    最复杂的宏是va_arg。

    #include <stdio.h>
    #include <stdarg.h>
    
    void var_test(char *format, ...)
    {
        va_list list;
        va_start(list,format);
        
        char *ch;
        while(1)
        {
             ch = va_arg(list, char *);
    
             if(strcmp(ch,"") == 0)
             {    
                   printf("\n");
                   break;
             }
             printf("%s ",ch);
         }
         va_end(list);
    }
    
    int main()
    {
        var_test("test","this","is","a","test","");
        return 0;
    }

    附:可变参数应用实例

    1.printf实现

    #include <stdarg.h>
    
    int printf(char *format, ...)
    {
        va_list ap;
        int n;
         
        va_start(ap, format);
        n = vprintf(format, ap);
        va_end(ap);
        return n;    
    }
    

    2.定制错误打印函数error

    #include  <stdio.h>
    #include  <stdarg.h>
    
    void error(char *format, ...)
    {
        va_list ap;
        va_start(ap, format);
        fprintf(stderr, "Error: ");
        vfprintf(stderr, format, ap);
        va_end(ap);
        fprintf(stderr, "\n");
        return;    
    }

     

    展开全文
  • va_list、va_start和va_end使用我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,由于1、硬件平台的不同 2、编译器的不同,所以定义的宏也有所不同。在ANSI C中,这些宏的定义位于stdarg.h中,典型的实现...

    va_list、va_start和va_end使用

    我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,由于1、硬件平台的不同 2、编译器的不同,所以定义的宏也有所不同。
    在ANSI C中,这些宏的定义位于stdarg.h中,典型的实现如下:typedef char *va_list;va_start宏,获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数最左边的参数):
    #define va_start(list,param1) ( list = (va_list)&param1+ sizeof(param1) )va_arg宏,获取可变参数的当前参数,返回指定类型并将指针指向下一参数(mode参数描述了当前参数的类型):
    #define va_arg(list,mode) ( (mode *) ( list += sizeof(mode) ) )[-1]va_end宏,清空va_list可变参数列表:
    #define va_end(list) ( list = (va_list)0 )
    注:以上sizeof()只是为了说明工作原理,实际实现中,增加的字节数需保证为为int的整数倍
    如:#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
    为了理解这些宏的作用,我们必须先搞清楚:C语言中函数参数的内存布局。首先,函数参数是存储在栈中的,函数参数从右往左依次入栈。
    以下面函数为讨论对象:

    089cdc450be3926b8f4d628ec3c746a9.gif

    089cdc450be3926b8f4d628ec3c746a9.gif


    void test(char *para1,char *param2,char *param3, char *param4) { va_list list; ...... return; }

    089cdc450be3926b8f4d628ec3c746a9.gif

    089cdc450be3926b8f4d628ec3c746a9.gif


    在linux中,栈由高地址往低地址生长,调用test函数时,其参数入栈情况如下:

    fe674ab0a03d82c725dc9d10c2e96df1.png


    当调用va_start(list,param1) 时:list指针指向情况对应下图:

    91a6a45c10cb40fa9a28993f4dbefbf8.png


    最复杂的宏是va_arg。它必须返回一个由va_list所指向的恰当的类型的数值,同时递增va_list,使它指向参数列表中的一个参数(即递增的大小等于与va_arg宏所返回的数值具有相同类型的对象的长度)。因为类型转换的结果不能作为赋值运算的目标,所以va_arg宏首先使用sizeof来确定需要递增的大小,然后把它直接加到va_list上,这样得到的指针再被转换为要求的类型。因为该指针现在指向的位置"过"了一个类型单位的大小,所以我们使用了下标-1来存取正确的返回参数。
    原地址:https://www.cnblogs.com/bettercoder/p/3488299.html
    ==================================================================================================================================================================================================(一)写一个简单的可变参数的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中,所以用到可变参数的程序应该包含这个
    头文件.下面我们写一个简单的可变参数的函数,改函数至少有一个整数
    参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.
    void simple_va_fun(int i, ...)
    {
    va_list arg_ptr;
    int j=0;
    va_start(arg_ptr, i);
    j=va_arg(arg_ptr, int);
    va_end(arg_ptr);
    printf("%d %dn", i, j);
    return;
    }
    我们可以在我们的头文件中这样声明我们的函数:
    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()改为:
    void simple_va_fun(int i, ...)
    {
    va_list arg_ptr;
    char *s=NULL;
    va_start(arg_ptr, i);
    s=va_arg(arg_ptr, char*);
    va_end(arg_ptr);
    printf("%d %sn", i, s);
    return;
    }
    可变参数为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的定义就够了,也便于程序的移植.
    1:当无法列出传递函数的所有实参的类型和数目时,可用省略号指定参数表
    void foo(...);
    void foo(parm_list,...);
    2:函数参数的传递原理
    函数参数是以数据结构:栈的形式存取,从右至左入栈.eg:
    先介绍一下可变参数表的调用形式以及原理:
    首先是参数的内存存放格式:参数存放在内存的堆栈段中,在执行函数的时候,从最后一个开始入栈。因此栈底高地址,栈顶低地址,举个例子如下:
    void func(int x, float y, char z);
    那么,调用函数的时候,实参 char z 先进栈,然后是 float y,最后是 int x,因此在内存中变量的存放次序是 x->y->z,因此,从理论上说,我们只要探测到任意一个变量的地址,并且知道其他变量的类型,通过指针移位运算,则总可以顺藤摸瓜找到其他的输入变量。
    下面是 <stdarg.h> 里面重要的几个宏定义如下:
    typedef char* va_list;
    void va_start ( va_list ap, prev_param ); /* ANSI version */
    type va_arg ( va_list ap, type );
    void va_end ( va_list ap );
    va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。

    • <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,应该养成获取完参数表之后关闭指针的习惯。
      例如 int max(int n, ...); 其函数内部应该如此实现:


    int max(int n, ...) { // 定参 n 表示后面变参数量,定界用,输入时切勿搞错
     va_list ap; // 定义一个 va_list 指针来访问参数表
    va_start(ap, n); // 初始化 ap,让它指向第一个变参,n之后的参数
    int maximum = -0x7FFFFFFF; // 这是一个最小的整数
    int temp;
    for(int i = 0; i < n; i++) {
    temp = va_arg(ap, int); // 获取一个 int 型参数,并且 ap 指向下一个参数
    if(maximum < temp) maximum = temp;
    }
    va_end(ap); // 善后工作,关闭 ap
    return max;
    }
    // 在主函数中测试 max 函数的行为(C++ 格式)
    int main() {
    cout << max(3, 10, 20, 30) << endl;
    cout << max(6, 20, 40, 10, 50, 30, 40) << endl;
    }
    基本用法阐述至此,可以看到,这个方法存在两处极严重的漏洞:其一,输入参数的类型随意性,使得参数很容易以一个不正确的类型获取一个值(譬如输入一个float,却以int型去获取他),这样做会出现莫名其妙的运行结果;其二,变参表的大小并不能在运行时获取,这样就存在一个访问越界的可能性,导致后果严重的 RUNTIME ERROR。

    #include <iostream> 
    void fun(int a, ...) 
    { 
    int *temp = &a; 
    temp++; 
    for (int i = 0; i < a; ++i) 
    { 
    cout << *temp << endl; 
    temp++; 
    } 
    }
    int main() 
    { 
    int a = 1; 
    int b = 2; 
    int c = 3; 
    int d = 4; 
    fun(4, a, b, c, d); 
    system("pause"); 
    return 0; 
    } 
    Output:: 
    1 
    2 
    3 
    4


    3:获取省略号指定的参数
    在函数体中声明一个va_list,然后用va_start函数来获取参数列表中的参数,使用完毕后调用va_end()结束。像这段代码:

    void TestFun(char* pszDest, int DestLen, const char* pszFormat, ...) 
    { 
    va_list args; 
    va_start(args, pszFormat); //一定要“...”之前的那个参数
    _vsnprintf(pszDest, DestLen, pszFormat, args); 
    va_end(args); 
    }


    4.va_start使argp指向第一个可选参数。va_arg返回参数列表中的当前参数并使argp指向参数列表中的下一个参数。va_end把argp指针清为NULL。函数体内可以多次遍历这些参数,但是都必须以va_start开始,并以va_end结尾。
      1).演示如何使用参数个数可变的函数,采用ANSI标准形式

    #include 〈stdio.h〉 
    #include 〈string.h〉 
    #include 〈stdarg.h〉 
    /*函数原型声明,至少需要一个确定的参数,注意括号内的省略号*/ 
    int demo( char, ... ); 
    void main( void ) 
    { 
       demo("DEMO", "This", "is", "a", "demo!", ""); 
    } 
    /*ANSI标准形式的声明方式,括号内的省略号表示可选参数*/ 
    int demo( char msg, ... ) 
    { 
           /*定义保存函数参数的结构*/
       va_list argp; 
       int argno = 0; 
       char para;
         /*argp指向传入的第一个可选参数,msg是最后一个确定的参数*/ 
       va_start( argp, msg ); 
       while (1) 
           { 
        para = va_arg( argp, char); 
           if ( strcmp( para, "") == 0 ) 
           break; 
           printf("Parameter #%d is: %s/n", argno, para); 
           argno++; 
    } 
    va_end( argp ); 
    /*将argp置为NULL*/
    return 0; 
    }


    2)//示例代码1:可变参数函数的使用

    #include "stdio.h"
    #include "stdarg.h"
    void simple_va_fun(int start, ...) 
    { 
        va_list arg_ptr; 
       int nArgValue =start;
        int nArgCout=0;     //可变参数的数目
        va_start(arg_ptr,start); //以固定参数的地址为起点确定变参的内存起始地址。
        do
        {
            ++nArgCout;
            printf("the %d th arg: %d/n",nArgCout,nArgValue);     //输出各参数的值
            nArgValue = va_arg(arg_ptr,int);                      //得到下一个可变参数的值
        } while(nArgValue != -1);                
        return; 
    }
    int main(int argc, char* argv[])
    {
        simple_va_fun(100,-1); 
        simple_va_fun(100,200,-1); 
        return 0;
    }


    3)//示例代码2:扩展——自己实现简单的可变参数的函数。
    下面是一个简单的printf函数的实现,参考了<The C Programming Language>中的例子

    #include "stdio.h"
    #include "stdlib.h"
    void myprintf(char* fmt, ...)        //一个简单的类似于printf的实现,//参数必须都是int 类型
    { 
        char* pArg=NULL;               //等价于原来的va_list 
        char c;
     
        pArg = (char*) &fmt;          //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值
        pArg += sizeof(fmt);         //等价于原来的va_start          
    
        do
        {
            c =*fmt;
            if (c != '%')
            {
                putchar(c);            //照原样输出字符
            }
            else
            {
               //按格式字符输出数据
               switch(*++fmt) 
               {
                case'd':
                    printf("%d",*((int*)pArg));           
                    break;
                case'x':
                    printf("%#x",*((int*)pArg));
                    break;
                default:
                    break;
                } 
                pArg += sizeof(int);               //等价于原来的va_arg
            }
            ++fmt;
        }while (*fmt != '/0'); 
        pArg = NULL;                               //等价于va_end
        return; 
    }
    int main(int argc, char* argv[])
    {
        int i = 1234;
        int j = 5678;
     
        myprintf("the first test:i=%d/n",i,j); 
        myprintf("the secend test:i=%d; %x;j=%d;/n",i,0xabcd,j); 
        system("pause");
        return 0;
    }
     
    展开全文
  • VA_LIST

    2020-05-20 14:46:16
    va_list原理解释: VA_LIST 是在C语言中解决变参问题的一组宏,在<stdarg.h>头文件下。 VA_LIST的用法: (1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针 (2)然后用VA_START宏初始化...

    va_list原理解释:

    VA_LIST 是在C语言中解决变参问题的一组宏,在<stdarg.h>头文件下。

    VA_LIST的用法:     
          (1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针
          (2)然后用VA_START宏初始化变量刚定义的VA_LIST变量,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数。
          (3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型。
          (4)最后用VA_END宏结束可变参数的获取。然后你就可以在函数里使用第二个参数了。如果函数有多个可变参数的,依次调用VA_ARG获取各个参数。
     

    展开全文
  • c语言实现可变参数的方式是利用函数参数入栈从右到左的的顺序,如果知道任意参数地址,且知道...再看va_list、va_start、va_arg、va_end,这几个是定义在stdarg.h的宏定义 typedef char* va_list; #define _INTSI...
  • va_list、va_start和va_end的用法

    千次阅读 多人点赞 2018-09-26 16:44:02
    在项目的代码中看到函数不定参数的使用,现在总结一下va_list、va_start和va_end三个宏的用法。 c语言提供了函数的不定长参数使用,比如 void func(int a, …)。三个省略号,表示了不定长参数。注意:c标准规定了,...
  • va_list、va_start、va_arg、va_end使用 #include “stdafx.h” #include <stdio.h> #include <stdarg.h> void Test(const char *lpszBuffer, int iLen, …) { printf("%s[%d]\n", lpszBuffer, iLen); ...
  • va_list

    千次阅读 2019-03-19 02:04:50
    VA_LIST 是在C语言中解决变参问题的一组宏,变参问题是指参数的个数不定,可以是传入一个参数也可以是多个;可变参数中的每个参数的类型可以不同,也可以相同. va_list 用法: 在函数里定义一具VA_LIST型的变量,这...
  • typedef char *va_list; va_start宏,获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数最左边的参数): #define va_start(list,param1) ( list = (va_list)&param1+ sizeof...
  • va_list、va_start和va_end使用我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,由于1、硬件平台的不同 2、编译器的不同,所以定义的宏也有所不同。在ANSI C中,这些宏的定义位于stdarg.h中,典型的实现...
  • typedef char *va_list; va_start宏,获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数最左边的参数): #define va_start(list,param1) ( list = (va_list)&param1+ sizeof...
  • va_list va_start va_arg

    2016-03-18 18:41:21
    摘自: ... typedef char* va_list; void va_start ( va_list ap, prev_param ); /* ANSI version */ type va_arg ( va_list ap, type );  v
  • va_start、va_arg、va_end、va_list的使用 VA_LIST 是在C语言中解决变参问题的一组宏 他有这么几个成员: 1) va_list型变量: #ifdef _M_ALPHA typedef struct {  char
  • #include <...//va_list 类型的变量,va_list arg_ptr //这个变量是指向参数地址的指针,因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。 //void va_end(va_list arg_ptr); //将
  • va_list/va_start/va_arg/va_end深入分析 va_list/va_start/va_arg/va_end这几个宏,都是用于函数的可变参数的。 我们来看看在vs2008中,它们是怎么定义的: 1: ///stdarg.h 2: #define va_start _crt...
  • 一, 函数的多个的参数的使用#include <stdarg.h>void va_start(va_list ap, last); type va_arg(va_list ap, type); void va_end(va_list ap); void va_copy(va_list dest, va_list src);va_list 类型的 在C语言中是...
  • va_list 键入以保存有关变量参数的信息 va_start 初始化变量参数列表 初始化ap以检索参数paramN后面的附加参数。 调用va_start的函数在返回之前也应调用va_end。 参数不能是引用类型,也不能是与传递没有参数的参数...
  • 本文主要介绍va_start和va_end的使用及原理。  在以前的一篇帖子Format MessageBox 详解中曾使用到va_start和va_end这两个宏,但对它们...void va_start ( va_list ap, prev_param ); /* ANSI version */ type va_ar
  • #ifdef _M_ALPHA typedef struct { char *a0; /* pointer to first homed integer argument */ int offset; /* byte offset of next parameter */ ...} va_list;...typedef char * va_list;
  • C语言va_list,va_start,va_arg,va_end需要注意: 在C语言中使用可变参数不利于编译器的语法检查,在使用时要慎重。#include #include #include <stdarg.h> /*define va_list,va_start,va_arg and va_end.*/void ...
  • 1.va_list等 先了解一下调用函数时,实参入栈的方式 stdarg.h中的定义 #inclde<stdarg.h> typedef char *va_list; #define va_start(list,param1) ( list = (va_list)&param1+ sizeof(param1) ) #define ...
  • va_list arg_ptr:定义一个指向个数可变的参数列表指针; va_start(arg_ptr, argN):使新建的参数列表指针arg_ptr指向函数参数列表(argn)中的第一个可选参数,例如: void ngx_log_stderr(int err, const char *fmt...
  • 最近在学野火的M3板子,发现串口的发送程序发现有va_list va_list va_start va_arg这些东东,以前没见到过啊。于是乎上网百度了一番,可得到出一下结论: va_list va_start va_arg va_end这些是用在C函数的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,620
精华内容 1,848
热门标签
关键字:

va_list