http://resource.lancetw.org/ebook/linux-c-on-site-learning-zh-tw/images/interface.vaarg.png

可變參數

到目前為止我們只見過一個帶有可變參數的函數printf


int printf(const char *format, ...);

以後還會見到更多這樣的函數。現在我們實現一個簡單的myprintf函數:


例 24.9. 用可變參數實現簡單的printf函數


#include
#include

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_startva_argva_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

圖 24.6. myprintf函數的參數佈局



http://resource.lancetw.org/ebook/linux-c-on-site-learning-zh-tw/images/interface.vaarg.png

這些參數是從右向左依次壓棧的,所以第一個參數靠近棧頂,第三個參數靠近棧底。這些參數在內存中是連續存放的,每個參數都對齊到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,例如NULLexecl(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結尾,如果調用者不遵守這個約定,實現者是沒有辦法避免錯誤的。


http://resource.lancetw.org/ebook/linux-c-on-site-learning-zh-tw/ch24s06.html





arrow
arrow
    全站熱搜

    立你斯 發表在 痞客邦 留言(0) 人氣()