變長參數表

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章