到目前為止我們只見過一個帶有可變參數的函數printf
:
int printf(const char *format, ...);
以後還會見到更多這樣的函數。現在我們實現一個簡單的myprintf
函數:
例 24.9. 用可變參數實現簡單的printf函數
#include <stdio.h> #include <stdarg.h> void myprintf(const char *format, ...) { va_list ap; char c; va_start(ap, format); while (c = *format++) { switch(c) { case 'c': { /* char is promoted to int when passed through '...' */ char ch = va_arg(ap, int); putchar(ch); break; } case 's': { char *p = va_arg(ap, char *); fputs(p, stdout); break; } default: putchar(c); } } va_end(ap); } int main(void) { myprintf("c\ts\n", '1', "hello"); return 0; }
要處理可變參數,需要用C到標準庫的va_list
類型和va_start
、va_arg
、va_end
宏,這些定義在stdarg.h
標頭檔中。這些宏是如何取出可變參數的呢?我們首先對照反彙編分析在調用myprintf
函數時這些參數的內存佈局。
myprintf("c\ts\n", '1', "hello"); 80484c5: c7 44 24 08 b0 85 04 movl $0x80485b0,0x8(%esp) 80484cc: 08 80484cd: c7 44 24 04 31 00 00 movl $0x31,0x4(%esp) 80484d4: 00 80484d5: c7 04 24 b6 85 04 08 movl $0x80485b6,(%esp) 80484dc: e8 43 ff ff ff call 8048424 <myprintf>
這些參數是從右向左依次壓棧的,所以第一個參數靠近棧頂,第三個參數靠近棧底。這些參數在內存中是連續存放的,每個參數都對齊到4位元組邊界。第一個和第三個參數都是指針類型,各占4個位元組,雖然第二個參數隻占一個位元組,但為了使第三個參數對齊到4位元組邊界,所以第二個參數也占4個位元組。現在給出一個stdarg.h
的簡單實現,這個實現出自[Standard C Library]:
例 24.10. stdarg.h的一種實現
/* stdarg.h standard header */ #ifndef _STDARG #define _STDARG /* type definitions */ typedef char *va_list; /* macros */ #define va_arg(ap, T) \ (* (T *)(((ap) += _Bnd(T, 3U)) - _Bnd(T, 3U))) #define va_end(ap) (void)0 #define va_start(ap, A) \ (void)((ap) = (char *)&(A) + _Bnd(A, 3U)) #define _Bnd(X, bnd) (sizeof (X) + (bnd) & ~(bnd)) #endif
這個標頭檔中的內部宏定義_Bnd(X, bnd)
將類型或變數X
的長度對齊到bnd+1
位元組的整數倍,例如_Bnd(char, 3U)
的值是4,_Bnd(int, 3U)
也是4。
在myprintf
中定義的va_list ap;
其實是一個指針,va_start(ap, format)
使ap
指向format
參數的下一個參數,也就是指向上圖中esp+4
的位置。然後va_arg(ap, int)
把第二個參數的值按int
型取出來,同時使ap
指向第三個參數,也就是指向上圖中esp+8
的位置。然後va_arg(ap, char *)
把第三個參數的值按char *
型取出來,同時使ap
指向更高的地址。va_end(ap)
在我們的簡單實現中不起任何作用,在有些實現中可能會把ap
改寫成無效值,C標準要求在函數返回前調用va_end
。
如果把myprintf
中的char ch = va_arg(ap, int);
改成char ch = va_arg(ap, char);
,用我們這個stdarg.h
的簡單實現是沒有問題的。但如果改用libc
提供的stdarg.h
,在編譯時會報錯:
$ gcc main.c main.c: In function ‘myprintf’: main.c:33: warning: ‘char’ is promoted to ‘int’ when passed through ‘...’ main.c:33: note: (so you should pass ‘int’ not ‘char’ to ‘va_arg’) main.c:33: note: if this code is reached, the program will abort $ ./a.out Illegal instruction
因此要求char
型的可變參數必須按int
型來取,這是為了與C標準一致,我們在第 3.1 節 “Integer Promotion”講過Default Argument Promotion規則,傳遞char
型的可變參數時要提升為int
型。
從myprintf
的例子可以理解printf
的實現原理,printf
函數根據第一個參數(格式化字元串)來確定後面有幾個參數,分別是什麼類型。保證參數的類型、個數與格式化字元串的描述相匹配是調用者的責任,實現者只管按格式化字元串的描述從棧上取數據,如果調用者傳遞的參數類型或個數不正確,實現者是沒有辦法避免錯誤的。
還有一種方法可以確定可變參數的個數,就是在參數列表的末尾傳一個Sentinel,例如NULL
。execl(3)
就採用這種方法確定參數的個數。下面實現一個printlist
函數,可以打印若干個傳入的字元串。
例 24.11. 根據Sentinel判斷可變參數的個數
#include <stdio.h> #include <stdarg.h> void printlist(int begin, ...) { va_list ap; char *p; va_start(ap, begin); p = va_arg(ap, char *); while (p != NULL) { fputs(p, stdout); putchar('\n'); p = va_arg(ap, char*); } va_end(ap); } int main(void) { printlist(0, "hello", "world", "foo", "bar", NULL); return 0; }
printlist
的第一個參數begin
的值並沒有用到,但是C語言規定至少要定義一個有名字的參數,因為va_start
宏要用到參數列表中最後一個有名字的參數,從它的地址開始找可變參數的位置。實現者應該在文檔中說明參數列表必須以NULL
結尾,如果調用者不遵守這個約定,實現者是沒有辦法避免錯誤的。