函数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》