请稍侯

可变参数的函数实现

20 September 2014

  提到可变参数的函数,大家第一个想到的肯定就是printf,在printf使用的时候,我们可以传入任意个参数,那么它到底是如何实现的呢?首先回忆下函数调用的过程,正常情况下C的函数参数入栈遵循__stdcall约定, 即从右到左将函数的参数压栈。如图所示,为了简明一点图中字节对齐填充没有画出来。PC上栈地址的增长是从高地址向低地址增长的,这就意味着最右边的参数地址最高,最左边的参数地址最低。而且参数压栈的是连续的,也就意味着参数的地址是连续的(考虑字节对齐),那我们是否在知道最左或者最右的参数的地址的情况下获得其他的参数呢?光知道最左或者最右的参数的地址还不够,我们还需要知道每个参数的类型才能确定每个参数的位置。
  可变参数的使用很简单,只要在至少一个的固定参数后面家加上分号和三个点即可,比如说:int printf(const char *format, ...)。头文件stdarg.h里面提供了一些宏定义可以让我们轻松的获得...传入的这些没有名字的参数。下面介绍下需要用到的这些宏定义。

typedef char* va_list;

#define __va_argsiz(t)  \
    (((sizeof(t) + sizeof(int) - 1) / sizeof(int)) * sizeof(int))

#define va_start(ap, pN)    \
    ((ap) = ((va_list) (&pN) + __va_argsiz(pN)))
 
#define va_arg(ap, t)                   \
     (((ap) = (ap) + __va_argsiz(t)),       \
      *((t*) (void*) ((ap) - __va_argsiz(t))))
   
#define va_end(ap)  ((void)0)

  上面代码找的是MinGW中stdarg.h中的,这些宏定义都会在printf中用到,首先,定义了一个类型va_list,也就是char*类型,用于指向那些可变的参数。
  va_start() :取出pN的地址,把它加上参数类型的大小(__va_argsiz是对大小进行4字节对齐)赋值给ap,初始化这些可变参数的列表.
  va_arg() :将ap指向的参数以及传入的类型信息返回,同时将把ap加上加上参数类型的大小(__va_argsiz是对大小进行4字节对齐)赋值给ap,让它指向下一个参数,为什么是加上当前参数的大小呢?因为我们是从左向右获取参数,地址从低到高,前一个参数的起始地址加上它的类型大小(经过字节对齐)得到的就是下一个参数的起始地址。
  va_end() :在获取参数结束之后调用,将表达式0显示的抛弃,有的实现代码中会将参数置为0表示结束。
我们来看一个例子,例子很简单,参数限定了只能是整形:

#include<stdio.h>
#include<stdarg.h>

void printargument(int num_args, ...)
{
    va_list arg_list;
    int my_arg;

    va_start(arg_list, num_args);

    //Print until zero
 for (my_arg = num_args; my_arg != 0; my_arg = va_arg(arg_list, int))
        printf("%d\n", my_arg);

    va_end(arg_list);
}

int main(void)
{
    printargument(5,10,15,0);
    return 0;
}

  函数首先定义了va_list类型(也就是char*类型)的变量arg_list,用于指向那些没有名字的参数,然后通过va_start宏定义将arg_list指向了第二个参数,然后继续for循环,首先打印第一个固定参数,然后va_arg宏定义将arg_list指向第三个参数,再将arg_list原来指向的第二个参数作为返回值返回,直到最后一个参数为0时退出循环,最后通过va_end表明结束,这里的va_end函数其实什么都没做,只是将0给显示的抛弃了,之前《最小值宏定义的解析》有说如果编译选项中有-Wunused-value选项(GCC警告选项,用来警告一个显式计算表达式的结果未被使用)时,编译会产生警告。这样就实现了一个简单的可变参数的函数。
  其实原理很简单,通过当前参数的地址在栈中加上偏移(当前参数的类型大小)即可得到下一个参数的起始地址。如果理解了这个函数,按那么看printf的代码就简单多了,就是对第一个固定参数的解析比较复杂,这里给出个源码大家可以自己看看。