zoukankan      html  css  js  c++  java
  • va_list va_start va_end

    VA函数(variable argument function),参数个数可变函数,又称可变参数函数。C/C++编程中,系统提供给编程人员的va函数很少。*printf()/*scanf() 系列函数,用于输入输出时格式化字符串;exec*()系列函数,用于在程序中执行外部文件(main(int argc, char* argv[]算不算呢,与其说main()也是一个可变参数函数,倒不如说它是exec*()经过封装后的具备特殊功能和意义的函数,至少在原理这一级上 有很多相似之处)。由于参数个数的不确定,使va函数具有很大的灵活性,易用性。

    一、 从printf()开始

    从大家都很熟悉的格式化字符串函数开始介绍可变参数函数。
    原型:int printf(const char * format, ...);
    参数format表示如何来格式字符串的指令,…
    表示可选参数,调用时传递给"..."的参数可有可无,根据实际情况而定。
    系统提供了vprintf系列格式化字符串的函数,用于编程人员封装自己的I/O函数。
    int vprintf / vscanf(const char * format, va_list ap); // 从标准输入/输出格式化字符串
    int vfprintf / vfsacanf(FILE * stream, const char * format, va_list ap); // 从文件流
    int vsprintf / vsscanf(char * s, const char * format, va_list ap); // 从字符串

     

    1 // 例1:格式化到一个文件流,可用于日志文件
    2  FILE *logfile;
    3  int WriteLog(const char * format, ...)
    4 {
    5 va_list arg_ptr;
    6 va_start(arg_ptr, format);
    7  int nWrittenBytes = vfprintf(logfile, format, arg_ptr);
    8 va_end(arg_ptr);
    9  return nWrittenBytes;
    10 }
    11
    12  // 调用时,与使用printf()没有区别。
    13  WriteLog("%04d-%02d-%02d %02d:%02d:%02d %s/%04d logged out.",
    14 nYear, nMonth, nDay, nHour, nMinute, szUserName, nUserID);

    同理,也可以从文件中执行格式化输入;或者对标准输入输出,字符串执行格式化。
    在上面的例1中,WriteLog()函数可以接受参数个数可变的输入,本质上,它的实现需要vprintf()的支持。如何真正实现属于自己的可变参数函数,包括控制每一个传入的可选参数。

    二、 va函数的定义和va宏

    C语言支持va函数,作为C语言的扩展--C++同样支持va函数,但在C++中并不推荐使用,C++引入的多态性同样可以实现参数个数可变的函数。不 过,C++的重载功能毕竟只能是有限多个可以预见的参数个数。比较而言,C中的va函数则可以定义无穷多个相当于C++的重载函数,这方面C++是无能为 力的。va函数的优势表现在使用的方便性和易用性上,可以使代码更简洁。C编译器为了统一在不同的硬件架构、硬件平台上的实现,和增加代码的可移植性,提 供了一系列宏来屏蔽硬件环境不同带来的差异。
    ANSI C标准下,va的宏定义在stdarg.h中,它们有:va_list,va_start(),va_arg(),va_end()。

    代码
    // 例2:求任意个自然数的平方和:
    int SqSum(int n1, ...)
    {
    va_list arg_ptr;
    int nSqSum = 0, n = n1;
    va_start(arg_ptr, n1);
    while (n > 0)
    {
    nSqSum
    += (n * n);
    n
    = va_arg(arg_ptr, int);
    }
    va_end(arg_ptr);
    return nSqSum;
    }
    // 调用时
    int nSqSum = SqSum(7, 2, 7, 11, -1);

    可变参数函数的原型声明格式为:
    type VAFunction(type arg1, type arg2, … );
    参数可以分为两部分:个数确定的固定参数和个数可变的可选参数。函数至少需要一个固定参数,固定参数的声明和普通函数一样;可选参数由于个数不确定,声明时用"…"表示。固定参数和可选参数公同构成一个函数的参数列表。
    借助上面这个简单的例2,来看看各个va_xxx的作用。
    va_list arg_ptr:定义一个指向个数可变的参数列表指针;
    va_start(arg_ptr, argN):使参数列表指针arg_ptr指向函数参数列表中的第一个可选参数,说明:argN是位于第一个可选参数之前的固定参数,(或者说,最后一个 固定参数;…之前的一个参数),函数参数列表中参数在内存中的顺序与函数声明时的顺序是一致的。如果有一va函数的声明是void va_test(char a, char b, char c, …),则它的固定参数依次是a,b,c,最后一个固定参数argN为c,因此就是va_start(arg_ptr, c)。
    va_arg(arg_ptr, type):返回参数列表中指针arg_ptr所指的参数,返回类型为type,并使指针arg_ptr指向参数列表中下一个参数。
    va_copy(dest, src):dest,src的类型都是va_list,va_copy()用于复制参数列表指针,将dest初始化为src。
    va_end(arg_ptr):清空参数列表,并置参数指针arg_ptr无效。说明:指针arg_ptr被置无效后,可以通过调用 va_start()、va_copy()恢复arg_ptr。每次调用va_start() / va_copy()后,必须得有相应的va_end()与之匹配。参数指针可以在参数列表中随意地来回移动,但必须在va_start() … va_end()之内。

    三、 编译器如何实现va

    例2中调用SqSum(7, 2, 7, 11, -1)来求7, 2, 7, 11的平方和,-1是结束标志。
    简单地说,va函数的实现就是对参数指针的使用和控制。
    typedef char *  va_list;  // x86平台下va_list的定义
    函数的固定参数部分,可以直接从函数定义时的参数名获得;对于可选参数部分,先将指针指向第一个可选参数,然后依次后移指针,根据与结束标志的比较来判断是否已经获得全部参数。因此,va函数中结束标志必须事先约定好,否则,指针会指向无效的内存地址,导致出错。
    这里,移动指针使其指向下一个参数,那么移动指针时的偏移量是多少呢,没有具体答案,因为这里涉及到内存对齐(alignment)问题,内存对齐跟具体 使用的硬件平台有密切关系,比如大家熟知的32位x86平台规定所有的变量地址必须是4的倍数(sizeof(int) = 4)。va机制中用宏_INTSIZEOF(n)来解决这个问题,没有这些宏,va的可移植性无从谈起。
    首先介绍宏_INTSIZEOF(n),它求出变量占用内存空间的大小,是va的实现的基础。
    #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 TestFunc(int n1, int n2, int n3, …) 参数传递时的内存堆栈情况。(C编译器默认的参数传递方式是__cdecl。)
    对该函数的调用为int result = TestFunc(a, b, c, d. e); 其中e为结束标志。


    从上图中可以很清楚地看出va_xxx宏如此编写的原因。
    1. va_start。为了得到第一个可选参数的地址,我们有三种办法可以做到:
    A) = &n3 + _INTSIZEOF(n3)
    // 最后一个固定参数的地址 + 该参数占用内存的大小
    B) = &n2 + _INTSIZEOF(n3) + _INTSIZEOF(n2)
    // 中间某个固定参数的地址 + 该参数之后所有固定参数占用的内存大小之和
    C) = &n1 + _INTSIZEOF(n3) + _INTSIZEOF(n2) + _INTSIZEOF(n1)
    // 第一个固定参数的地址 + 所有固定参数占用的内存大小之和
    从编译器实现角度来看,方法B),方法C)为了求出地址,编译器还需知道有多少个固定参数,以及它们的大小,没有把问题分解到最简单,所以不是很聪明的途 径,不予采纳;相对来说,方法A)中运算的两个值则完全可以确定。va_start()正是采用A)方法,接受最后一个固定参数。调用 va_start()的结果总是使指针指向下一个参数的地址,并把它作为第一个可选参数。在含多个固定参数的函数中,调用va_start()时,如果不 是用最后一个固定参数,对于编译器来说,可选参数的个数已经增加,将给程序带来一些意想不到的错误。(当然如果你认为自己对指针已经知根知底,游刃有余, 那么,怎么用就随你,你甚至可以用它完成一些很优秀(高效)的代码,但是,这样会大大降低代码的可读性。)
    注意:宏va_start是对参数的地址进行操作的,要求参数地址必须是有效的。一些地址无效的类型不能当作固定参数类型。比如:寄存器类型,它的地址不是有效的内存地址值;数组和函数也不允许,他们的长度是个问题。因此,这些类型时不能作为va函数的参数的。
    2. va_arg身兼二职:返回当前参数,并使参数指针指向下一个参数。
    初看va_arg宏定义很别扭,如果把它拆成两个语句,可以很清楚地看出它完成的两个职责。
    #define va_arg(ap,t)   ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //下一个参数地址
    // 将( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )拆成:
    /* 指针ap指向下一个参数的地址 */
    1.        ap += _INTSIZEOF(t);        // 当前,ap已经指向下一个参数了
    /* ap减去当前参数的大小得到当前参数的地址,再强制类型转换后返回它的值 */
    2.        return *(t *)( ap - _INTSIZEOF(t))
    回想到printf/scanf系列函数的%d %s之类的格式化指令,我们不难理解这些它们的用途了- 明示参数强制转换的类型。
    (注:printf/scanf没有使用va_xxx来实现,但原理是一致的。)
    3.va_end很简单,仅仅是把指针作废而已。
    #define va_end(ap) (ap = (va_list)0) // x86平台

    四、 简洁、灵活,也有危险

    从va的实现可以看出,指针的合理运用,把C语言简洁、灵活的特性表现得淋漓尽致,叫人不得不佩服C的强大和高效。不可否认的是,给编程人员太多自由空间必然使程序的安全性降低。va中,为了得到所有传递给函数的参数,需要用va_arg依次遍历。其中存在两个隐患:
    1)如何确定参数的类型。 va_arg在类型检查方面与其说非常灵活,不如说是很不负责,因为是强制类型转换,va_arg都把当前指针所指向的内容强制转换到指定类型;
    2)结束标志。如果没有结束标志的判断,va将按默认类型依次返回内存中的内容,直到访问到非法内存而出错退出。例2中SqSum()求的是自然数的平方 和,所以我把负数和0作为它的结束标志。例如scanf把接收到的回车符作为结束标志,大家熟知的printf()对字符串的处理用'\0'作为结束标 志,无法想象C中的字符串如果没有'\0', 代码将会是怎样一番情景,估计那时最流行的可能是字符数组,或者是malloc/free。
    允许对内存的随意访问,会留给不怀好意者留下攻击的可能。当处理cracker精心设计好的一串字符串后,程序将跳转到一些恶意代码区域执行,以使cracker达到其攻击目的。(常见的exploit攻击)所以,必需禁止对内存的随意访问和严格控制内存访问边界。

    五、 Unix System V兼容方式的va声明

    上面介绍可变参数函数的声明是采用ANSI标准的,Unix System V兼容方式的声明有一点点区别,它增加了两个宏:va_alist,va_dcl。而且它们不是定义在stdarg.h中,而是varargs.h中。 stdarg.h是ANSI标准的;varargs.h仅仅是为了能与以前的程序保持兼容而出现的,现在的编程中不推荐使用。
    va_alist:函数声明/定义时出现在函数头,用以接受参数列表。
    va_dcl:对va_alist的声明,其后无需跟分号";"
    va_start的定义也不相同。因为System V可变参数函数声明不区分固定参数和可选参数,直接对参数列表操作。所以va_start()不是va_start(ap,v),而是简化为va_start(ap)。其中,ap是va_list型的参数指针。
    Unix System V兼容方式下函数的声明形式:
    type VAFunction(va_alist)
    va_dcl  // 这里无需分号
    {
        // 函数体内同ANSI标准
    }
    // 例3:猜测execl的实现(Unix System V兼容方式),摘自SUS V2
    #include
    #define MAXARGS    100
    / * execl(file, arg1, arg2, ..., (char *)0); */
    execl(va_alist)
    va_dcl
    {
        va_list ap;
        char *file;
        char *args[MAXARGS];
        int argno = 0;
        va_start(ap);
        file = va_arg(ap, char *);
        while ((args[argno++] = va_arg(ap, char *)) != (char *)0)
            ;
        va_end(ap);
        return execv(file, args);
    }

  • 相关阅读:
    java编译错误No enclosing instance of type TestFrame is accessible. Must qualify the allocation with an enclosing instance of type TestFrame (e.g. x.new A(
    java 2中创建线程方法
    动态规划基本思想
    关于eclipse编译一个工程多个main函数
    java Gui初识
    Eclipse中java项目的打包
    java 播放声音
    把资源文件夹导入到eclipse中
    Java建立JProgressBar
    How to grant permissions to a custom assembly that is referenced in a report in Reporting Services
  • 原文地址:https://www.cnblogs.com/BloodAndBone/p/1938053.html
Copyright © 2011-2022 走看看