• 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语言中函数参数的内存布局。首先,函数参数是存储在栈中的,函数参数从右往左依次入栈。

    以下面函数为讨论对象:

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

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

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

    最复杂的宏是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 %d ", 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 %s ", 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:: 



    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;
    }

    原地址:https://blog.csdn.net/holandstone/article/details/8268732

  • 相关阅读:
    工具类---xlsx文件读写
    2021上半年第二次作业总结
    2021上半年第一次作业总结
    C语言II博客作业04
    C语言II—作业03
    C语言II博客作业02
    C语言II博客作业01
    win7开启snmp服务实现监控过程展现
    台湾某医学会sql注入漏洞
    测试面试题(持续总结中)
  • 原文地址:https://www.cnblogs.com/coolYuan/p/10181011.html
Copyright © 2020-2023  润新知