首先在介紹可變參數表函數的設計之前,我們先來介紹一下最經典的可變參數表printf函數的實現原理。
一、printf函數的實現原理
在C/C++中,對函數參數的掃描是從後向前的。C/C++的函數參數是通過壓入堆棧的方式來給函數傳參數的(堆棧是一種先進後出的數據結構),最先壓入的參數最後出來,在計算機的內存中,數據有2塊,一塊是堆,一塊是棧(函數參數及局部變量在這裡),而棧是從內存的高地址向低地址生長的,控制生長的就是堆棧指針了,最先壓入的參數是在最上面,就是說在所有參數的最後面,最後壓入的參數在最下面,結構上看起來是第一個,所以最後壓入的參數總是能夠被函數找到,因為它就在堆棧指針的上方。printf的第一個被找到的參數就是那個字符指針,就是被雙引號括起來的那一部分,函數通過判斷字符串裡控制參數的個數來判斷參數個數及數據類型,通過這些就可算出數據需要的堆棧指針的偏移量了,下面給出printf("%d,%d",a,b);(其中a、b都是int型的)的匯編代碼
你會看到,參數是最後的先壓入棧中,最先的後壓入棧中,參數控制的那個字符串常量是最後被壓入的,所以這個常量總是能被找到的。
二、可變參數表函數的設計
標准庫提供的一些參數的數目可以有變化的函數。例如我們很熟悉的printf,它需要有一個格式串,還應根據需要為它提供任意多個“其他參數”。這種函數被稱作“具有變長度參數表的函數”,或簡稱為“變參數函數”。我們寫程序中有時也可能需要定義這種函數。要定義這類函數,就必須使用標准頭文件<stdarg.h>,使用該文件提供的一套機制,並需要按照規定的定義方式工作。本節介紹這個頭文件提供的有關功能,它們的意義和使用,並用例子說明這類函數的定義方法。
C中變長實參頭文件stdarg.h提供了一個數據類型va-list和三個宏(va-start、va-arg和va-end),用它們在被調用函數不知道參數個數和類型時對可變參數表進行測試,從而為訪問可變參數提供了方便且有效的方法。va-list是一個char類型的指針,當被調用函數使用一個可變參數時,它聲明一個類型為va-list的變量,該變量用來指向va-arg和va-end所需信息的位置。下面給出va_list在C中的源碼:
typedef char * va_list;
void va-start(va-list ap,lastfix)是一個宏,它使va-list類型變量ap指向被傳遞給函數的可變參數表中的第一個參數,在第一次調用va-arg和va-end之前,必須首先調用該宏。va-start的第二個參數lastfix是傳遞給被調用函數的最後一個固定參數的標識符。va-start使ap只指向lastfix之外的可變參數表中的第一個參數,很明顯它先得到第一個參數內存地址,然後又加上這個參數的內存大小,就是下個參數的內存地址了。下面給出va_start在C中的源碼:
type va-arg(va-list ap,type)也是一個宏,其使用有雙重目的,第一個是返回ap所指對象的值,第二個是修改參數指針ap使其增加以指向表中下一個參數。va-arg的第二個參數提供了修改參數指針所必需的信息。在第一次使用va-arg時,它返回可變參數表中的第一個參數,後續的調用都返回表中的下一個參數,下面給出va_arg在C中的源碼:
#define va_arg(ap,type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) ) //將參數轉換成需要的類型,並使ap指向下一個參數
在使用va-arg時,要注意第二個參數所用類型名應與傳遞到堆棧的參數的字節數對應,以保證能對不同類型的可變參數進行正確地尋址,比如實參依次為char型、char * 型、int型和float型時,在va-arg中它們的類型則應分別為int、char *、int和double.
void va-end(va-list ap)也是一個宏,該宏用於被調用函數完成正常返回,功能就是把指針ap賦值為0,使它不指向內存的變量。下面給出va_end在C中的源碼:
#define va_end(ap) ( ap = (va_list)0 )
va-end必須在va-arg讀完所有參數後再調用,否則會產生意想不到的後果。特別地,當可變參數表函數在程序執行過程中不止一次被調用時,在函數體每次處理完可變參數表之後必須調用一次va-end,以保證正確地恢復棧。
一個變參數函數至少需要有一個普通參數,其普通參數可以具有任何類型。在函數定義中,這種函數的最後一個普通參數除了一般的用途之外,還有其他特殊用途。下面從一個例子開始說明有關的問題。
假設我們想定義一個函數sum,它可以用任意多個整數類型的表達式作為參數進行調用,希望sum能求出這些參數的和。這時我們應該將sum定義為一個只有一個普通參數,並具有變長度參數表的函數,這個函數的頭部應該是(函數原型與此類似):
int sum(int n, ...)
我們實際上要求在函數調用時,從第一個參數n得到被求和的表達式個數,從其余參數得到被求和的表達式。在參數表最後連續寫三個圓點符號,說明這個函數具有可變數目的參數。凡參數表具有這種形式(最後寫三個圓點),就表示定義的是一個變參數函數。注意,這樣的三個圓點只能放在參數表最後,在所有普通參數之後。
下面假設函數sum裡所用的va_list類型的變量的名字是vap。在能夠用vap訪問實際參數之前,必須首先用宏a_start對這個變量進行初始化。宏va_start的類型特征可以大致描述為:
va_start(va_list vap, 最後一個普通參數)
在函數sum裡對vap初始化的語句應當寫為:
va_start(vap, n); 相當於 char *vap= (char *)&n + sizeof(int);
此時vap正好指向n後面的可變參數表中的第一個參數。