C語言中有一種長度不確定的參數,形如:"...",它主要用在參數個數不確定的函數中,我們最容易想到的例子是printf函數。
C語言用va_start等宏來處理這些可變參數。其實原理挺簡單,就是根據參數入棧的特點從最靠近第一個可變參數的固定參數開始,依次擷取每個可變參數的地址。
標準C語言中標頭檔<stdarg.h>專門用來對付可變參數列表,它包含了一組宏和一個va_list的typedef聲明。不同平台有不同的定義,X86下的宏定義:
typedef char * va_list;#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 )
_INTSIZEOF(n)宏是為了考慮那些記憶體位址需要對齊的系統(從此可以看出編譯器產生參數調用時,必須是滿足一定的對齊規則的).
為了能從固定參數依次得到每個可變參數,va_start,va_arg充分利用下面兩點:
1.C語言在函數調用時,先將最後一個參數壓入棧(C語言的函數是從右向左壓入堆棧的)
2.X86平台下的記憶體配置順序是從高地址記憶體到低地址記憶體
<高位地址>
第N個可變參數
第N-1個可變參數
......
第2個可變參數
第1個可變參數 ap
固定參數 v
<低位地址>
- 可見,&v是固定參數在記憶體中的地址,在調用va_start後,ap指向第一個可變參數。這個宏的作用就是在v的記憶體位址上增加v所佔的記憶體大小,這樣就得到了第一個可變參數的地址。
- 接下來,可以這樣設想,如果我能確定這個可變參數的類型,那麼我就知道了它佔用了多少記憶體,我就能得到下一個可變參數的地址。
- va_arg的目的是返回當前ap指向的值並且將ap後移,它先將ap指向下一個可變參數,然後減去當前可變參數的大小即得到當前可變參數的記憶體位址,再做個類型轉換,返回它的值。
- 要確定每個可變參數的類型,有兩種做法,要麼都是預設的類型,要麼就在固定參數中包含資訊讓程式可以確定每個可變參數的類型(比如printf,分析format字串就可以確定每個可變參數的類型)
- va_end宏是使ap不再指向有效記憶體位址。
總的使用原則是:
(1)首先在函數裡定義一個va_list型的變數,這裡是arg_ptr,這個變數是指向參數的指標.
(2)然後用va_start宏初始設定變數arg_ptr,這個宏的第二個參數是第一個可變參數的前一個參數,是一個固定的參數.
(3)然後用va_arg返回可變的參數的值,並賦值給j,va_arg的第二個參數是你要返回的參數的類型,這裡是int型.
(4)最後用va_end宏結束可變參數的擷取.(如果函數有多個可變參數的,依次調用va_arg擷取各個參數)
(5)這三個宏的作用只是用來確定可變參數列表中每個參數的記憶體位址,編譯器是不知道參數的實際數目的。程式員必須自己考慮確定參數數目的辦法,如
a)在固定參數中設標誌 -- printf函數就是用這個辦法。
b)預先設定一個特殊的結束標記,就是說多輸入一個可變參數,調用時要將最後一個可變參數的值設定成這個特殊的值,在函數體中根據這個值判斷是否達到參數的結尾。
(6)實現可變參數的要點就是想辦法取得每個參數的地址,取得地址的辦法由以下幾個因素決定:
a)函數棧的生長方向
b)參數的入棧順序
c)CPU的對齊
d)記憶體位址的表達方式
(7)取得地址後,再結合參數的類型,程式員就可以正確的處理參數了。
//Example: void simple_va_fun(int i, ...) { va_list arg_ptr; int j=0; va_start(arg_ptr, i); j=va_arg(arg_ptr, int); va_end(arg_ptr); printf("%d %d\n", i, j); return; }
//-----------------------------------------------------------------------------
【問題】:有沒有辦法寫一個函數,這個函數參數的具體形式可以在運行時才確定?
目前沒有"正規"的解決辦法,不過獨門偏方倒是有一個,因為有一個函數已經給我們做出了這方面的榜樣,那就是main(),它的原型是:
int main(int argc,char *argv[]);
深入想一下,"只能在運行時確定參數形式",也就是說你沒辦法從聲明中看到所接受的參數,也即是參數根本就沒有固定的形式。
常用的辦法是你可以通過定義一個void*類型的參數,用它來指向實際的參數區,然後在函數中根據根據需要任意解釋它們的含義。
這就是main函數中argv的含義,而argc,則用來表明實際的參數個數,這為我們使用提供了進一步的方便,當然,這個參數不是必需的。
雖然參數沒有固定形式,但我們必然要在函數中解析參數的意義,因此,理所當然會有一個要求,就是調用者和被調者之間要對參數區
內容的格式,大小,有效性等所有方面達成一致,否則南轅北轍各說各話就慘了。
【問題】:我想使用va_arg來提取出可變長參數中類型為函數指標的參數,結果卻總是不正確,為什嗎?
這個與va_arg的實現有關。一個簡單的、示範版的va_arg實現如下:
#define va_arg(argp, type) (*(type *)(((argp) += sizeof(type)) - sizeof(type)))
其中,argp的類型是char*。如果你想用va_arg從可變參數列表中提取出函數指標類型的參數,例如int (*)(),
則va_arg(argp, int (*)())被擴充為:
(*(int (*)() *)(((argp) += sizeof (int (*)())) -sizeof (int (*)())))
顯然,(int (*)() *)是無意義的。
解決這個問題的辦法是將函數指標用typedef定義成一個獨立的資料類型,例如:
typedef int (*funcptr)();
這時候再調用va_arg(argp, funcptr)將被擴充為:
(* (funcptr *)(((argp) += sizeof (funcptr)) - sizeof (funcptr)))
這樣就可以通過編譯了。
【問題】:有這樣一個具有可變長參數的函數,其中有下列代碼用來擷取類型為float的實參:
va_arg (argp, float);
這樣做可以嗎?
不可以。在可變長參數中,應用的是"加寬"原則。也就是float類型被擴充成double;char,short被擴充成int。
因此,如果你要取可變長參數列表中原來為float類型的參數,需要用va_arg(argp, double)。對char和short類型的則用va_arg(argp, int)。