可變參數
在C/C++函式中使用可變參數
下面介紹在C/C++裡面使用的可變參數函式。
先說明可變參數是什麼,先回顧一下C++裡面的函式重載,如果重複給出如下聲明:
int func();
int func(int);
int func(float);
int func(int, int);
...
這樣在調用相同的函式名 func 的時候,編譯器會自動識別入參列表的格式,從而調用相對應的函式體。
但這樣的方法畢竟有限,試想一下我們假如想定義一個函式,我們在調用之前(在運行期之前)根本不知道我到底要調用幾個參數,並且不知道這些參數是個什麼類型,例如我們想定義一個函式:
int max(int n, ...);
用來返回一串隨意長度輸入參數的最大值,例如調用
max(3, 10, 20, 30)的時候,可以返回(n=3)個數 10,20,30 的最大值30。
並且還可以接受任意個參數的輸入,例如:
max(6, 20, 40, 10, 50, 30, 40)也應該是被接受的,返回最大值50。
這怎么達到呢?
其實這樣的例子我們肯定見過,最典型的就是 printf 函式,可以看 printf 函式的原形:
int printf(char*, ...);
標準C庫
它接受一個格式字元串,並且後面跟隨任意指定的參數,根據實際需要而確定入參的個數。
實際上它的實現要依賴於一個標準 C 庫 ,standard argument(標準參數) 的意思。下面先稍為介紹一下 ,或者在 C++ 中的 的功效:
這實際上是一組初始化和調用可變參數的宏,下面先介紹一下可變參數表的調用形式以及原理:
首先是參數的記憶體存放格式:參數存放在記憶體的堆疊段中,在執行函式的時候,從最後一個開始入棧。因此棧底高地址,棧頂低地址,舉個例子如下:
void func(int x, float y, char z);
那么,調用函式的時候,實參 char z 先進棧,然後是 float y,最後是 int x,因此在記憶體中變數的存放次序是 x->y->z,因此,從理論上說,我們只要探測到任意一個變數的地址,並且知道其他變數的類型,通過指針移位運算,則總可以順藤摸瓜找到其他的輸入變數。
然後是可變入參表格式,省略的參數用 ... 代替,但必須注意:
1. 只能有一個 ... 並且它必須是最後一個參數;
2. 不要只用一個 ... 作為所有的參數,因為從後面可以知道,這樣你無法確定入參表的地址。
舉個例子,聲明函式如下:
void func(int x, int y, ...);
然後調用:func(3, 5, 'c', 2.1f, 6);
於是在調用參數的時候,編譯器則不會檢查實際輸入的是什麼參數,只管把所有參數按照上面描述的方法,變成實參堆放在記憶體中,在本例中,記憶體中依次存放 x=3, y=5, 'c', 2.1f, 6
但是有一個需要注意的地方,這些東西只是緊挨著堆放在記憶體中,於是想要正確調用這些參數,必須知道他們確切的類型,並且我們也關心這個參數表實際的長度,然而不幸的是,這些我們無從得知。因此,這個解決辦法決不是高明的,從某種程度上說,這甚至是一個嚴重的漏洞。因此,C++ 很不提倡去使用它。
不過缺點歸缺點,萬不得已的時候我們還是得用,但是我們對裡面輸入變數的時候,應該對入參的類型有一個清醒的認識,否則這樣的操作是很危險的。
下面是 對上面這一個思路的實現,裡面重要的幾個宏定義如下:
typedef char* va_list;
void va_start ( va_list ap, prev_param ); /* ANSI version */
type va_arg ( va_list ap, type );
void va_end ( va_list ap );
其中,va_list 是一個字元指針,可以理解為指向當前參數的一個指針,取參必須通過這個指針進行。
使用步驟
在調用參數表之前,應該定義一個 va_list 類型的變數,以供後用(下面假設這個 va_list 類型變數被定義為ap);
然後應該對 ap 進行初始化,讓它指向可變參數表裡面的第一個參數,這是通過 va_start 來實現的,第一個參數是 ap 本身,第二個參數是在變參表前面緊挨著的一個變數;
然後是獲取參數,調用 va_arg,它的第一個參數是 ap,第二個參數是要獲取的參數的指定類型,然後返回這個指定類型的值,並且把 ap 的位置指向變參表的下一個變數位置;
獲取所有的參數之後,我們有必要將這個 ap 指針關掉,以免發生危險,方法是調用 va_end,他是輸入的參數 ap 置為 NULL,應該養成獲取完參數表之後關閉指針的習慣。
例子
例如開始的例子 int max(int n, ...); 其函式內部應該如此實現:
int max(int n, ...) { // 定參 n 表示後面變參數量,定界用,輸入時切勿搞錯
va_list ap; // 定義一個 va_list 指針來訪問參數表
va_start(ap, n); // 初始化 ap,讓它指向第一個變參
int maximum = -0x7FFFFFFF; // 這是一個最小的整數
int temp;
for(int i = 0; i < n; i++) {
temp = va_arg(ap, int); // 獲取一個 int 型參數,並且 ap 指向下一個參數。這裡取的是整數所以是這樣,但是如果是比較字元串則使用的char*,這裡需要注意的是short char,他們使用時需要轉換成int型,這裡也可直接寫成int
if(maximum < temp) maximum = temp;
}
va_end(ap); // 善後工作,關閉 ap
return maximum ;
}
// 在主函式中測試 max 函式的行為(C++ 格式)
int main() {
cout << max(3, 10, 20, 30) << endl;
cout << max(6, 20, 40, 10, 50, 30, 40) << endl;
}
存在不足
基本用法闡述至此,可以看到,這個方法存在兩處極嚴重的漏洞:
其一,
輸入參數的類型隨意性,使得參數很容易以一個不正確的類型獲取一個值(譬如輸入一個float,卻以int型去獲取他),這樣做會出現莫名其妙的運行結果;
其二,
變參表的大小並不能在編譯時獲取,這樣就存在一個訪問越界的可能性,導致後果嚴重的 RUNTIME ERROR。
另外, 的內部實現形式在這處不再加說明,如果有需要可以參考下面的兩個連線(感謝他們的作者)。
建議
作為建議,在 C++ 環境中儘量不要使用這種方法,如有需要,儘量先考慮使用類或者重載來代替,這樣可以很好地彌補這種方法的漏洞。
全文完感謝讀者,ELF原創,轉載請註明出處
bitou補充:
這裡面有一個例子,求參數的平均值,這些參數是可變參數,現在將函式奉上:
#include
float average(int n_value,...)//可變參數函式
{
va_list var_arg;//va_list類型變數用於訪問參數列表中未定義的部分
int count;
float sum = 0;
va_start(var_arg,n_value);//va_start宏將var_arg指定為可變參數部分的第一個參數
for(count=0; count
{
sum += va_arg(var_arg,int);//va_arg返回var_arg的值,並指向參數列表中的下一個參數
}
va_end(var_arg);//訪問完最後一個可變參數之後,調用va_end宏終止使用可變參數
return sum/n_value;
}