它接受乙個格式字串,並且後面跟隨任意指定的引數,根據實際需要而確定入參的個數。
實際上它的實現要依賴於乙個標準 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 ); /*初始化引數指標ap,將函式引數a右邊第乙個引數的位址賦給ap。 a必須是乙個引數的指標,所以此種型別函式至少要有乙個普通的引數。*/
type va_arg ( va_list ap, type ); /* 獲得ap指向引數的值,並使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,應該養成獲取完參數列之後關閉指標的習慣。
#include
#include
using namespace std;
int max(int n, ...)
va_end(ap); // 善後工作,關閉 ap
return maximum ;
}// 在主函式中測試 max 函式的行為(c++ 格式)
int main()
基本用法闡述至此,可以看到,這個方法存在兩處極嚴重的漏洞:
輸入引數的型別隨意性,使得引數很容易以乙個不正確的型別獲取乙個值(譬如輸入乙個float,卻以int型去獲取他),這樣做會出現莫名其妙的執行結果;
變參表的大小並不能在編譯時獲取,這樣就存在乙個訪問越界的可能性,導致後果嚴重的 runtime error。
c語言通過幾個巨集實現變參的定址。下面是linux2.18核心原始碼裡這幾個巨集的定義,相信符合c89,c99標準的c語言基本都是這樣定義的。
typedef char *va_list;
/*storage alignment properties -- 堆疊按機器字對齊
*/#define _aupbnd (sizeof (acpi_native_uint) - 1)
#define _adnbnd (sizeof (acpi_native_uint) - 1)
/*variable argument list macro definitions -- 變參函式內部實現需要用到的巨集
*/#define _bnd(x, bnd) (((sizeof (x)) + (bnd)) & (~(bnd)))
#define va_arg(ap, t) (*(t *)(((ap) += (_bnd (t, _aupbnd))) - (_bnd (t,_adnbnd))))
#define va_end(ap) (void) 0
#define va_start(ap, a) (void) ((ap) = (((char *) &(a)) + (_bnd (a,_aupbnd))))
下面以x86 32位機為例分析這幾個巨集的用途
要理解這幾個巨集需要對c語言如何傳遞引數有一定了解。與pascal相反,與stdcall 相同,c語言傳遞引數時是用push指令從右到左將引數逐個壓棧,因此c語言裡通過棧指標來訪問引數。雖然x86的push一次可以壓2,4或8個位元組入棧,c語言在壓引數入棧時仍然是機器字的size為最小單位的,也就是說引數的位址都是字對齊的,這就是_bnd(x,bnd)存在的原因。另外補充一點常識,不管是彙編還是c,編譯出的x86函式一般在進入函式體後立即執行
push ebp
mov ebp, esp
這兩條指令。首先把ebp入棧,然後將當前棧指標賦給ebp,以後訪問棧裡的引數都使用ebp作為基指標。
一一解釋這幾個巨集的作用。
_bnd(x,bnd) ,計算型別為x的引數在棧中佔據的位元組數,當然是字對齊後的位元組數了。acpi_native_unit是乙個機器字,32位機的定義是:typedef u32 acpi_native_uint;
顯然,_aupbnd ,_adnbnd 的值是 4-1 == 3 == 0x00000003 ,按位取反( ~(bnd))就是0xfffffffc 。
因此,_bnd(x,bnd) 巨集在32位機下就是
( (sizeof(x) + 3)&0xfffffffc )
很明顯,其作用是--倘若sizeof(x)不是4的整數倍,去餘加4。
_bnd(sizeof(char),3) == 4
_bnd(sizeof(struct size7struct),3) == 8
有空自己再寫多個例子。
C C 可變引數函式
可變引數的函式,即函式的引數個數和引數型別不完全確定的函式。這類函式最常見的就是printf scanf函式。在c c 中,為了通知編譯器函式有可變引數,必須以三個點結束該函式的宣告。例如 printf函式的宣告 int printf const char format,scanf函式宣告 int ...
C C 可變引數函式
標頭檔案 include 函式宣告 int add int count,函式定義 int add int count,函式呼叫 int main 邊長引數模板相當於乙個模板的遞迴展開模型,但是它不是遞迴的。使用的時候,要定義乙個 遞迴 的出口,然後定義一系列的操作,操作的是以 遞迴 的方式進行的。遞...
可變引數的c c 函式
最近在寫遊戲的時候看到書上寫了個virtual void displaytext int id,long x,long y,unsigned long color,char text,0 當我看到這個函式時,我和小夥伴們都驚呆啦,我的娘,這是神馬函式,還帶省略號的,難道是書上寫錯了?然後我抱著試試看...