http://baike.baidu.com/view/1340814.htm
可變參數
在C/C++函數中使用可變參數
作者轉自:http://foggy-elves.blog.sohu.com/
下面介紹在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 庫 <stdarg.h>
它接受一個格式字串,並且後面跟隨任意指定的參數,根據實際需要而確定入參的個數。
實際上它的實現要依賴於一個標準 C 庫 <stdarg.h>,standard argument(標準參數) 的意思。下面先稍為介紹一下 <stdarg.h>,或者在 C++ 中的 <cstdarg> 的功效:
這實際上是一組初始化和調用可變參數的宏,下面先介紹一下可變參數表的調用形式以及原理:
首先是參數的記憶體存放格式:參數存放在記憶體的堆棧段中,在執行函數的時候,從最後一個開始入棧。因此棧底高地址,棧頂低地址,舉個例子如下:
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++ 很不提倡去使用它。
不過缺點歸缺點,萬不得已的時候我們還是得用,但是我們對裡面輸入變數的時候,應該對入參的類型有一個清醒的認識,否則這樣的操作是很危險的。
下面是 <stdarg.h> 對上面這一個思路的實現,裡面重要的幾個宏定義如下:
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 是一個字元指標,可以理解為指向當前參數的一個指標,取參必須通過這個指標進行。
編輯本段使用<stdarg.h>步驟<Step 1>
在調用參數表之前,應該定義一個 va_list 類型的變數,以供後用(下面假設這個 va_list 類型變數被定義為ap);
<Step 2>
然後應該對 ap 進行初始化,讓它指向可變參數表裡面的第一個參數,這是通過 va_start 來實現的,第一個參數是 ap 本身,第二個參數是在變參表前面緊挨著的一個變數;
<Step 3>
然後是擷取參數,調用 va_arg,它的第一個參數是 ap,第二個參數是要擷取的參數的指定類型,然後返回這個指定類型的值,並且把 ap 的位置指向變參表的下一個變數位置;
<Step 4>
擷取所有的參數之後,我們有必要將這個 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 指向下一個參數
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。
另外,<stdarg.h> 的內部實現形式在這處不再加說明,如果有需要可以參考下面的兩個串連(感謝他們的作者)。[1-2]
編輯本段建議
作為建議,在 C++ 環境中盡量不要使用這種方法,如有需要,盡量先考慮使用類或者重載來代替,這樣可以很好地彌補這種方法的漏洞。
全文完感謝讀者,ELF原創,轉載請註明出處
bitou補充:
這裡面有一個例子,求參數的平均值,這些參數是可變參數,現在將函數奉上:
#include <stdarg.h>
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<n_value;count +=1)
{
sum += va_arg(var_arg,int);//va_arg返回var_arg的值,並指向參數列表中的下一個參數
}
va_end(var_arg);//訪問完最後一個可變參數之後,調用va_end宏終止使用可變參數
return sum/n_value;
}
http://baike.baidu.com/view/1081188.htm
vfprintf 函數名: vfprintf
功 能: 格式化的資料輸出到指定的資料流中
用 法: int vfprintf(FILE *stream, char *format, va_list param);
函數說明:vfprintf()會根據參數format字串來轉換並格式化資料,然後將結果輸出到參數stream指定的檔案中,直到出現字串結束(‘\0’)為止。關於參數format字串的格式請參 考printf()。
傳回值:成功則返回實際輸出的字元數,失敗則返回-1,錯誤原因存於errno中。
程式例:
#include
#include
#include
FILE *fp;
int vfpf(char *fmt, ...)
{
va_list argptr;
int cnt;
va_start(argptr, fmt);
cnt = vfprintf(fp, fmt, argptr);
va_end(argptr);
return(cnt);
}
int main(void)
{
int inumber = 30;
float fnumber = 90.0;
char string[4] = "abc";
fp = tmpfile();
if (fp == NULL)
{
perror("tmpfile() call");
exit(1);
}
vfpf("%d %f %s", inumber, fnumber, string);
rewind(fp);
fscanf(fp,"%d %f %s", &inumber, &fnumber, string);
printf("%d %f %s\n", inumber, fnumber, string);
fclose(fp);
return 0;
}
http://baike.baidu.com/view/531918.htm
函數簡介
函數名: abort
標頭檔:#include <stdlib.h>
功 能: 異常終止一個進程。中止當前的過程,返回一個錯誤碼。錯誤碼的預設值是3
中止一個程式異常終止的過程。
該函數產生SIGABRT訊號,預設情況下導致程式終止不成功的終止錯誤碼返回到主機環境。
自動或靜態儲存期間的對象,而無需調用任何atexit函數,解構函式不執行程式終止。函數永遠不會返回到其調用者。
用 法: void abort(void);
編輯本段舉例
程式例:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("Calling abort()\n");
abort();
return 0; /* This is never reached */
}