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