可变参数列表剖析

来源:互联网 发布:淘宝怎样设置自动回复 编辑:程序博客网 时间:2024/05/17 23:01

  在普通函数中,形参的个数是固定的,调用函数时,通过实参与形参的虚实结合实现函数的调用。为了使函数能够在不同的情况下接收不同数目的参数,我们可使用可变参数列表。
  下面用一个例子具体介绍可变参数列表的使用:

//求最大值#include <stdio.h>#include <windows.h>#include <stdarg.h>#include <assert.h>int MAX(int n, ...){    assert(n);    va_list arg;        //定义变量arg    int i = 0;    int sum = 0;    int max = 0;    va_start(arg, n);    //初始化arg    max = va_arg(arg, int);    for (i = 1; i < n; i++)    {        sum = va_arg(arg, int);  //提取数值        if (max < sum)        {            max = sum;        }    }    return max;    va_end(arg);    //将arg指向空}int main(){    int max = MAX(8,1,3,14,2,8,5,9,12);    printf("max = %d\n", max);    system("pause");    return 0;}

首先我们来了解几个概念:
   va_list:声明一个变量 //va_list arg;
   va_start:初始化va_list声明的这个变量,它的第一个参数是va_list声明的变量,另外一个参数是省略号前最后一个有名字的参数//va_start(arg,n);
  初始化过程就是arg变量设置为指向可变参数部分的第一个参数。
   va_arg : 我们就是通过这个来访问未知参数的,va_arg的一个参数是va_list声明的这个变量,另一个参数是列表中下一个要接收的参数类型//va_arg(arg,int);
   va_end : 释放空间

  在c语言中:
  在进行参数调用时,会从右到左把参数压入栈中,对于MAX(8,1,3,14,2,8,5,9,12)的调用,其栈内布局如图1所示,因为我们在MAX函数中已经设了n一个形参,所以最上面的一个参数值也就是8可以通过访问n直接得到,那么我们如何得到3,14,2,8,5,9,12这几个参数?这时候就需要va_list,也就是一个指针,我们首先定义一个va_list类型的参数arg,这时候arg指向的内容是不确定的。
  调用va_start(arg,n)初始化va_list声明的这个变量,此时arg指向的便是可变参数部分的第一个参数。因为va_list是系统自定义的,所以我们一般不要直接取值,而是调用va_arg(arg,int),它的返回值是arg指针指向的内容,并且将arg下移一位,int表示的是参数的类型,如图2所示。这样在for循环中我们就可以将所有的参数取出,当然也就可以得到最终的最大值。
  需要注意的时,在退出MAX函数前,必须调用va_end,否则可能会导致不可预料的结果。

栈帧
栈帧

  通过这个例子,可知在使用可变参数列表时应思考两个问题:
  1.怎么得到参数的值。
  对于一般的函数,我们可以通过参数对应在参数列表里的标识符来得到。但是参数可变函数那些可变的参数是没有参数标识符的,它只有“…”,所以通过标识符来得到是不可能的,我们只能另寻他法。
  我们知道函数调用时都会分配栈空间,而函数调用机制中的栈结构如下图所示:

栈
  可见,参数是连续存储在栈里面的,也就是说,我们只要得到可变参数的前一个参数的地址,就可以通过指针访问到那些可变参数。但是怎么样得到可变参数的前一个参数的地址呢?其实很容易发现参数可变函数在可变参数之前必有一个参数是固定的,并使用了标识符。这样的话,我们就可以通过这个参数对应的标识符来得到地址,从而访问其他参数。
  2.怎样确定参数类型和数量
  通过上述的方式,我们解决了取得可变参数值的问题,但是对于一个参数,值很重要,其类型同样举足轻重,而对于一个函数来讲参数个数也非常重要,否则就会产生了一系列的麻烦。通过访问存储参数的栈空间,我们并不能得到关于类型的任何信息和参数个数的任何信息。我想你应该想到了——使用char * 参数。printf函数就是这样实现的,它把后面的可变参数类型都放到了char * 指向的字符数组里,并通过%来标识以便与其它的字符相区别,从而确定了参数类型也确定了参数个数。其实,用何种方式来到达这样的效果取决于函数的实现。 比如说,定义一个函数,预知它的可变参数类型都是int,那么固定参数完全可以用int类型来替换char * 类型,因为只要得到参数个数就可以了。

注意:
可变参数的限制:
  1.可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但如果你想一开始就访问参数列表中间的参数,那是不行的。
  2.参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用va_start。
  3.这些宏是无法直接判断实际存在参数的数量。
  4.这些宏无法判断每个参数的类型。

观察VS中的源码:

源码

可见可变参数的实现过程是使用宏的封装。只要完成替换,就可自行分析了。