變長引數學習筆記
1、簡介
在定義介面時,經常會遇到引數個數甚至型別都不確定的情況。這時,在類 C 語言中我們可以使用省略號指定引數表,具體形式如下:
void fun(parm1,parm2,...);
這種傳參形式被稱為變長引數
。C 語言中的:
int printf(const char * format, ...)
便是兩個最經典的例子。對於固定引數列表的函式,每個引數的名稱、型別是直接可見的,他們的地址和值也可以直接得到。但是對於變長引數的函式,該如何獲取這些資訊呢?
2、實現原理
函式的引數在記憶體中是以從右到左的順序依次存放在棧中,最右側的引數最先入棧,最左邊的引數最後入棧,比如:
void func(int x, float y, char z);
在發生函式呼叫的時候,形參 z 先進棧,然後是 y,最後是 x,最終在記憶體中幾個變數的存放次序是 x->y->z。
按照 C 標準,支援變長引數的函式宣告中,必須至少在最左側有一個固定引數。根據前文所述,形參在記憶體中是存放在棧中,而且順序是連續的。因此,有了最左側的固定引數
和可變引數的型別
,我們就能獲取到所有的可變引數的地址和值。
3、變長引數獲取
3.1 獲取
在 C 語言中<stdarg.h>
檔案中定義了幾個用於獲取變長引數的巨集:
- va_list
typedef char* va_list;
va_list 是一個字元指標,可以理解為指向當前引數的一個指標,所有對變長引數的獲取都需要通過這個指標進行。因此,在獲取變長引數之前,需要先定義一個 va_list 型別的變數,比如叫ap
。
- va_start
void va_start(va_list ap, param);
ap
定義好了後,需要通過 va_start 初始化,讓它指向變長引數列表中的第一個。該函式的第一個引數就是前面定義好的ap
,第二個引數則是變長引數表前面緊挨著的變數(即...
之前的那個)。
- va_arg
type va_arg(va_list ap, type);
接下來便可以通過 va_arg 來按順序獲取變長引數列表中的每一個引數。該方法第一個引數是ap
,第二個引數是當前要獲取的變長引數的型別;該方法的返回值便是當前要獲取的引數值;每呼叫一次以後,便把ap
指向了下一個變數的位置。
- va_end
void va_end(va_list ap);
全部引數獲取結束以後,需要呼叫 va_end 把ap
指標關掉,以保證程式健壯性。因此,通常 va_start 和 va_end 是成對出現。
3.2 內部實現
在 VC++ 的 <stdarg.h> 裡, x86 平臺的上述巨集定義實現如下 :
typedef char * va_list; #define _INTSIZEOF(n) \ ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) #define va_arg(ap,t) \ ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #define va_end(ap) ( ap = (va_list)0 )
其中,_INTSIZEOF 的實現方式時為了保證獲取到的大小是 int 的整數倍。其它的巨集實現就比較容易理解了。
4、變長引數應用
我們可以寫一個簡單版的 printf 來展示該方法的實現原理和變長引數的獲取方法:
void mineprintf(char *fmt, ...) { va_list ap; char *p, *sval; int ival; double dval; va_start(ap, fmt); for (p = fmt; *p; p++) { if (*p != '%') { putchar(*p); continue; } switch (*++p) { case 'd': ival = va_arg(ap, int); printf("%d",ival); break; case 'f': dval = va_arg(ap, double); printf("%f",dval); break; case 's': for (sval = va_arg(ap, char *); *sval; sval++) { putchar(*sval); } break; default: putchar(*p); break; } } va_end(ap); }