正在查看旧版本。 查看 当前版本.

与当前比较 查看页面历史记录

版本 1 下一个 »

函数除了可以接收固定的参数之外,还可以接受不确定的函数参数,称为变参函数,典型的变参函数是我们已经用得非常熟练的printf和scanf函数,它们都可以接受不确定个数的参数,它们的函数声明形式如下:

int printf(const char *format, ...);
int scanf(const char *format, ...);

可以发现上面两个函数的声明都有一个共同点,那就是都含有一个占位符...。这个...并不是参数,而是告诉编译器,该函数是变参函数,不管该函数使用时参数有多少个,都对其一一做压栈处理,这就实现了变参函数。

变参函数的实现与密切相关,栈是一种数据结构,是一种只能在一端进行插入和删除的特殊线性表(线性表可以暂时理解成数组)。栈的存储规律是先进后出(First In Last Out),先入栈的数据被压入栈底,最后入栈的数据在栈顶,需要读数据时则从栈顶开始弹出数据(最后一个数据被第一个弹出来)。

一个栈中,栈底是固定的,而栈顶是浮动的,对栈的插入和删除操作,不需要改变栈底的位置。

在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。在i386机器中,栈底位于高地址,栈顶位于低地址,压栈(PUSH)使得栈顶地址变小,弹栈(POP)(也可以称为退栈)使栈顶地址变大。

栈在程序的运行中有着举足轻重的作用。栈可以用来在函数调用时存储断点信息,做递归时要用到栈。最重要的是栈保存了一个函数调用时所需要的维护信息,通常称为堆栈帧,保存函数的返回地址和参数,以及函数的局部变量。

下面我们来分析变参函数参数的压栈过程。一般来说,函数参数的入栈顺序是从右向左的,意味着,最右边的参数最先入栈,位于高地址处,而最左边的参数最后入栈,位于低地址。下面通过一个具体的例子来看函数的压栈操作。

#include <stdio.h>
void print(int n, ...)
{
    int *p, i;
    p = &n + 1;
    for (i = 0; i < n; i++)
    {
        printf("%d\t", *(p + i));
    }
    printf("\n");
    return;
}

int main()
{
    print(4, 12, 34, 56, 78);
    return 0;
}

运行结果:
12 34 56 78

以上代码,首先在print函数中使用占位符“…”,因此该函数在编译时被当成变参函数来处理,对该函数调用中的参数将一一进行压栈。上述代码定义了一个int型指针p,由于函数参数的压栈顺序是从右向左,参数的存储的地址由高地址到低地址,所以p = &n + 1得到的是指向第一个可变参数的地址,接下来通过循环一一取出函数中的参数。

在使用变参函数时,在“…”前面至少要有一个普通的参数。必须知道参数什么时候结束,如果没有给出变参函数的个数,直接结出第一个参数,则必须约定一个参数作为结束标志。

当然,在C语言中,系统也提供了用于实现变参函数的宏va_list, va_start, va_arg, va_end,它们的一种可能定义如下:

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)

下面通过这些宏来重写print变参函数:

void print(int n, …)
{
 int arg, i;
 va_list p;
 va_start(p, n);
 for(i = 0; i < n; i++)
 {
  arg = va_arg(p, int);
  printf(“%d\t”, arg);
 }
 printf(“\n”);
 va_end(p);
 return;
}

这里要注意的一点是,函数的压栈是按照4字节对齐的,小于4字节的统统按4字节对齐来入栈,而这里的_INTSIZEOF宏,就是用于实现内存中的字节对齐操作。宏va_start(ap, v)的作用是先得到变量v的地址,然后将其转化成char型指针,再加上v对齐之后所占用的内存大小,使指针指向下一个参数。注意此时的指针为char类型,所以接下来在使用va_arg(ap, t)时要将其强制转换为当前参数类型t的指针。对于宏va_arg(ap, t)要注意的是,“ap += _INTSIZEOF(t)”得到的是下一个参数的地址,再减去_INTSIZEOF(t)得到当前参数的地址。通过一个for循环就可以一一取出其中压栈的所有参数。最后一个宏va_end(ap)的作用清除指针,表示在接下来的部分不再使用该指针变量。

//通过变参函数的宏实现一个类似printf()的函数
void myprintf(const char *fmt, ...)
{
 va_list p;
 char c; 
 va_start(p, fmt);
 while(*fmt != '\0')
 {
  c = *fmt;
  if(c != '%')
  {
   putchar(c);
  } 
  else 
  {
   fmt++;
   switch(*fmt)
   {
    case 'd': 
     printf("%d", *((int*)p)); 
     va_arg(p, int);
     break;
    case 'c': 
     printf("%c", *((int*)p)); 
     va_arg(p, int);
     break;
    case 'f': 
     printf("%f", *((double*)p));
     va_arg(p, double);
     break;
   } 
  }
  fmt++;
 } //end while
 va_end(p);
 return;
}


  • 无标签