变长参数的实现

我们用的最多的C函数是哪个?毫无疑问,是printf。但是你看过printf的声明式吗,那是相当诡异。随便拿一本带C库函数参考的书,可查到如下结果:
             int printf(const char *format, ...);
那三个连续的点就代表大于或等于0个参数,再加上前面的format参数,所以printf函数至少要接受一个字符串,后面就随便了。但是这是如何实现的呢,不急,首先必须要了解C语言函数参数传递的机制。

C语言函数参数传递机制

我们知道,每个程序都有个用户栈,所有的函数调用都要使用它,比如被调用函数的实参及局部变量都要存在用户栈中,所以每个函数都有个栈帧。下面我用两个函数来说明这种调用机制:
int main(void)                             int add(int param1, int param2)
{                                          {
    int a, b, c;                               int d;
    a = 2;                                     d = param1 + param2;
    b = 3;                                     return param1+param2;
    c = add(a,b);                          }
    return 0;

}
main函数在调用add函数时,main负责把b,a及eip(即返回地址)按顺序压栈,注意栈是向下增长的。然后执行call指令调用add函数。(ps: 其实在返回地址和局部变量d之间函数add还会压入一些寄存器,但是这对我们理解变长参数没有影响,总之要记住参数是自右向左压栈的)
                              

处理变长参数的宏定义
处理变长参数说白了就是怎样获取那些在声明中没明说的参数,比如在printf("%d equals to %d.", a, b)中,第一个字符串参数可直接通过format取得,但a和b呢?这就要需要对参数传递的理解了。主函数调用printf时肯定是先压入b,再压入a,最后压入"%d equals to %d"的地址。
现在我们来获取a的地址,很简单,当然是 (char*)&format+sizeof(const char*)。其中&format是参数format在栈中的地址,而a正好format上面。

OK, 现在我们可以系统地讲解处理变长参数的宏了。
在头文件stdarg.h中先定义个类型 va_list,它是专门用来去可变参数的,如果要对付可变参数首先要定义一个va_list类型的变量ap:
            typedef char* va_list;
因为在获取参数地址时必须知道它前一个参数的数据类型大小,所以要定义__va_sizeof(type),它的作用和sizeof一样,不过__va_sizeof(char),__va_sizeof(short)的值都是4:
            #define __va_sizeof(type) (((sizeof(type) + sizeof(int) - 1) / sizeof(int)) * sizeof(int))      
然后定义va_start获取第一个可变参数的地址:
            #define va_start(ap, last) ((ap) = (va_list)&(last) + __va_size(last))
va_arg用于获取ap当前指向的参数的值,并顺带把va_list移向下一个参数
            #define va_arg(ap, type) (*(type *)((ap) += __va_size(type), (ap) - __va_size(type)))
当用完ap时,用va_end将其赋值为空,也就是0
            #define va_end(ap) (ap)=0


处理变长参数的一般框架
如果我们自己实现printf函数,代码的形式大概如下
int chparamf(const char *format, ...)
{
    va_list ap;
    va_start(ap,format);
    循环使用 va_arg(ap,你要取的参数的数据类型)
    va_end(ap);
}

posted on 2009-11-08 13:51  John Waken  阅读(1388)  评论(0编辑  收藏  举报

导航