变长参数表

    函数printf的正确声明形式为:

int printf(char *fmt, ...);

     其中,省略号表示参数表中参数的数量和类型是可变的(省略号只能出现在参数表的尾部)。类似的参数表被称为边长参数表。它除了有一个参数fmt固定以外,后面跟的参数的个数和类型是可变的(用三个点“…”做参数占位符)。


    在《C程序设计语言》中,Ritchie提供了一个简易版printf函数minprintf:

#include <stdarg.h>

void minprintf(char *fmt, ...)
{
    va_list ap;    /* 依次指向每个无名参数 */
    char *p, *sval;
    int ival;
    double dval;

    va_start(ap, fmt);    /* 将ap指向第一个无名参数 */
    for (p = fmt; *p; p++) {
        if(*p != '%') {
            putchar(*p);
            continue;
        }
        switch(*++p) {
        case 'd':
            ival = va_arg(ap, int);
            printf("%d", ival);
            break;
        case 'f':
            dval = va_arg(ap, double);
            printf("%f", dval);
            break;
        case 's':
            for (sval = va_arg(ap, char *); *sval; sval++)
                putchar(*sval);
            break;
        default:
            putchar(*p);
            break;
        }
    }
    va_end(ap);    /* 结束时的清理工作 */
}

以上代码很简单,编写函数minprintf的关键在于如何处理一个甚至连名字都没有的参数表。下面我们从标准头文件<stdarg.h>说起。


    <stdarg.h>中包含一组宏定义,它们对如何遍历参数表进行了定义。该头文件的实现因不同的机器而不同,但提供的接口是一致的。

typedef char *   va_list;     /* 其中va表示variable argument可变参数*/
#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 )

    下面我们解释这些代码的含义。


1、va_list类型用于声明一个变量,该变量将依次引用各参数。被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的;


2、_INTSIZEOF(n)咋一看有点令人费解,其实它主要是为了内存对齐,其原理可参考《_INTSIZEOF(n)解析》;


3、va_start(ap, v),其参数ap为va_list类型,v为确定的参数fmt。其作用是初始化可变参数列表(把函数在fmt之后的参数地址放到ap中);


4、va_arg(ap, t)(t表示用户输入的类型type),( *(t *)( (ap += _INTSIZEOF(t)) - _INTSIZEOF(t) ) )这个式子不仔细看也会让人不解,ap怎么先加上_INTSIZEOF(t)有减去它,这不多此一举吗?其实不然,注意括号,ap+=自身变了,接着ap只是参与这个表达式计算而已,ap不会再变了。因此这个宏做了两件事:

(1)用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值;

(2)计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。


5、va_end(ap),x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的。


在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的。


参考文献:

1、http://blog.chinaunix.net/uid-2413049-id-109789.html

2、《The C Programming Language》

你可能感兴趣的:(c,printf,变长参数表)