stdarg

在C/C++函式中使用可變參數。首先是參數的記憶體存放格式:參數存放在記憶體的堆疊段中,在執行函式的時候,從最後一個開始入棧。其中,va_list 是一個字元指針,可以理解為指向當前參數的一個指針,取參必須通過這個指針進行。變參表的大小並不能在編譯時獲取,這樣就存在一個訪問越界的可能性,導致後果嚴重的 RUNTIME ERROR。另外, 的內部實現形式在這處不再加說明,如果有需要可以參考下面的兩個連線(感謝他們的作者)。作為建議,在 C++ 環境中儘量不要使用這種方法,如有需要,儘量先考慮使用類或者重載來代替,這樣可以很好地彌補這種方法的漏洞。

可變參數

在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;

}

相關詞條

相關搜尋

熱門詞條

聯絡我們