變長參數的函數即參數個數可變、參數類型不定 的函數。
設計一個參數個數可變、參數類型不定的函數是可能的,最常見的例子是printf函數、scanf函數和進階語言的Format函數。在C/C++中,為了通知編譯器函數的參數個數和類型可變(即是不定的、未知的),就必須以三個點結束該函數的聲明。
view plaincopy to
clipboardprint?
- // printf函數的聲明
-
- int printf(const char * _Format, ...);
-
- //scanf函式宣告
-
- int scanf(const char * _Format, ...);
-
- //自訂變長參數函數func的聲明
-
- int func(int a,int b,...);
// printf函數的聲明</p><p>int printf(const char * _Format, ...);</p><p>//scanf函式宣告</p><p>int scanf(const char * _Format, ...);</p><p>//自訂變長參數函數func的聲明</p><p>int func(int a,int b,...);<br />
上面func函數的聲明指出該函數至少有兩個整型參數和緊隨其後的0個或多個類型未知的參數。在C/C++中,任何使用變長參數聲明的函數都必須至少有一個指定的參數(又稱強制參數),即至少有一個參數的類型是已知的,而不能用三個點省略所有參數的指定,且已知的指定參數必須聲明在函數最左端。
view plaincopy to
clipboardprint?
- //下面這種聲明是非法的
-
- int func(...);//錯誤
-
- int func(...,int a);//錯誤
//下面這種聲明是非法的</p><p>int func(...);//錯誤</p><p>int func(...,int a);//錯誤
變長參數函數的實現
含有變長參數的函數是怎麼實現的呢?變長參數函數的實現其實關鍵在於怎麼使用參數,指定了的參數好說,直接使用指定的參數名稱訪問,但未指定的參數呢?我們知道函數調用過程中參數傳遞是通過棧來實現的,一般調用都是從右至左的順序壓參數入棧,因此參數與參數之間是相鄰的,知道前一個參數的類型及地址,根據後一個參數的類型就可以擷取後一個參數的內容。對於變長參數函數,結合一定的條件,我們可以根據最後一個指定參數擷取之後的省略參數內容。如,對於函數func,我們知道了參數b的地址及類型,就可知道第一個可變參數的棧地址(如果有的話),如果知道第一個可變參數的類型,就可知道第一個可變參數的內容和第二個可變參數的地址(如果有的話)。以此類推,可以實現對可變參數函數的所有參數的訪問。
那麼,要怎麼指定上訴的“一定的條件”呢?最簡單的方法就像printf等函數一樣,使用格式化預留位置。分析格式化字串參數,通過事先定義好的格式化預留位置可知可變參數的類型及個數,從而擷取各個參數內容。一般對於可變參數類型相同的函數也可直接在強制參數中指定可變參數的個數和類型,這樣也能擷取各個參數的內容。
無論哪種,都涉及對棧地址位移的操作。結合棧儲存模式和系統資料類型的字長,我們可根據可變參數的類型很容易得到棧地址的位移量。這裡簡單介紹使用va_start、va_arg、va_end三個標準宏來實現棧地址的位移及擷取可變參數內容。這三個宏定義在stdarg.h標頭檔中,他們可根據預先定義的系統平台自動擷取相應平台上各個資料類型的位移量。
view plaincopy to
clipboardprint?
- //訪問可變參數流程
-
- va_list args; //定義一個可變參數列表
-
- va_start(args,arg);//初始化args指向強制參數arg的下一個參數;
-
- va_arg(args,type);//擷取當前參數內容並將args指向下一個參數
-
- ...//迴圈擷取所有可變參數內容
-
- va_end(args);//釋放args
//訪問可變參數流程</p><p>va_list args; //定義一個可變參數列表</p><p>va_start(args,arg);//初始化args指向強制參數arg的下一個參數;</p><p>va_arg(args,type);//擷取當前參數內容並將args指向下一個參數</p><p>...//迴圈擷取所有可變參數內容</p><p>va_end(args);//釋放args<br />
實現一個簡單的變長參數函數:
view plaincopy to
clipboardprint?
- //sum為求和函數,其參數類型都為int,但參數個數不定
- //第一個參數(強制參數)n指定後面有多少可變參數
- int sum(unsigned int n,...)
- {
- int sum=0;
- va_list args;
- va_start(args,n);
- while(n>0)
- {
- //通過va_arg(args,int)依次擷取參數的值
- sum+=va_arg(args,int);
- n--;
- }
- va_end(args);
- return sum;
- }
//sum為求和函數,其參數類型都為int,但參數個數不定<br />//第一個參數(強制參數)n指定後面有多少可變參數<br />int sum(unsigned int n,...)<br />{<br /> int sum=0;<br /> va_list args;<br /> va_start(args,n);<br /> while(n>0)<br /> {<br /> //通過va_arg(args,int)依次擷取參數的值<br /> sum+=va_arg(args,int);<br /> n--;<br /> }<br /> va_end(args);<br /> return sum;<br />}
對於可變參數函數的調用有一點需要注意,實際的可變參數的個數必須比前面強制參數中指定的個數要多,或者不小於,也即後續參數多一點不要緊,但不能少,如果少了則會訪問到函數參數以外的堆棧地區,這可能會把程式搞崩掉。前面強制參數中指定的類型和後面實際參數的類型不符也有可能造成程式崩潰。
變長參數函數與預設參數函數
擁有變長參數的函數在聲明定義時其參數個數與類型是不定的,在運行調用時參數的狀態則是一定的。而預設參數函數在聲明定義時其參數類型與個數都是一定的,只是後面部分參數指定了預設值,可通過省略(不指定)部分參數調用這個預設參數函數。但是預設參數函數還是使用了聲明中指定的全部參數,只不過編譯器做了個順水人情,自動給後部分參數賦了預設值;而變長參數函數則僅僅使用了運行調用時提供的參數。