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