入棧規則
可變參數函數的實現與函數調用的棧幀結構是密切相關的。所以在我們實現可變參數之前,先得搞清楚 棧是怎樣傳參的。
正常情況下,C的函數參數入棧遵照__stdcall規則, 它是從右到左的,即函數中的參數入棧是從右到左的。
例如:
1 void test(char a, int b,double c,char * d){ 2 printf("a:%#p\nb:%#p\nc:%#p\nd:%#p",&a,&b,&c,d); 3 } 4 int main(){ 5 char ch; 6 test('a',12,23,&ch); 7 return 0; 8 }

從各個形參變量的地址可以看出它們地址大小確實是從右到左依次減小的,說明它們是從右到左壓棧的,
實現原理
對於固定參數列表的函數,每個參數的名稱、類型都是直接可見的,他們的地址也都是可以直接得到的,比如:通過&a就可以得到a的地址,並通過函數原型聲明了解到a是char類型的。
對於變長參數的函數,怎么辦呢?其實想想函數傳參的過程,無論"..."中有多少個參數、每個參數是什么類型的,它們都和固定參數的傳參過程是一樣的,簡單來講都是棧操作,同時C標准的說明中,是支持變長參數的函數在原型聲明中的,但須至少有一個最左固定參數,嘿嘿~ 這樣,我們不就可以得到其中固定參數的地址了嗎?
知道了某函數幀的棧上的一個固定參數的位置,所以我們完全可以自己通過棧操作,推導出其他變長參數的位置,進而實現可變參數函數。(這個“固定的參數”一般就是可變參數函數里在第一個位置的參數,通過它就可以開始找到后面各種類型、個數不定的參數了)
(上述說了一下實現原理,知道的大佬就請忽略咯~)
實現步驟
我們常用的可變參數列表有這幾個:
1.va_list
1 源碼:typedef char * va_list;
va_list為char*類型重定義,所以va_list為一個指向char類型的指針(va_list p就等同於 char *p)
2.va_start(ap,v)
源碼:1.#define va_start _crt_va_start
2.#define _crt_va_start(ap,v) (ap=(va_list)_ADDRESSOF(v)+ _INTSIZEOF(V))
把v的地址強轉為va_list類型即char* ,把其移動_INTSIZEOF(V)個字節后的地址賦值給ap,其實就是讓ap跳過第一個參數,指向"..."里的第一個可變參數。
(這里這個_ADDRESSOF(v)是一個宏,對變量v取地址的意思;這個INSIZEOF(v)也是宏,是對變量v向上取4的倍數,
也就是說如果v占字節大小在1~4個字節范圍內,就取4,v所占字節大小在5~8字節之間就取8,以此類推...
至於這里,_ADDRESSOF(v),_INTSIZEOF(V)這兩個宏怎么實現,后面有小弟我一點淺淺的見解~ )
3.va_arg(ap,t)
源碼:1.#define va_arg _crt_va_arg
2.#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
它的作用就是將ap移動_INTSIZEOF(t)個字節后,然后再取出ap未移動前所指向位置對應的數據。
下面假使t為一個int型變量,如下圖分析

4.va_end(ap)
源碼:1.#define va_end _crt_va_end
2.#define _crt_va_end(ap) ( ap = (va_list)0 )
將0強轉為va_list類型,並賦值給ap,使其置空
於是這樣就可以開始實現my_print函數
#include<stdio.h>
#include<assert.h> #include<stdarg.h> void putInt(int n){ if(n>9){ putInt(n/10); } putchar(n%10+ '0'); } int My_print(const char *formt, ...) { assert(formt); va_list arg;//定義arg va_start(arg, formt);//初始化arg 即跳過傳進來的第一個參數 ,這里相當於跳過"output:>%s %c %c %d"這個字符串 const char *start=formt; while (*start!= '\0') { if(*start =='%'){ start++; switch(*start) { case 'd': putInt(va_arg(arg, int)); break; case 'c': putchar(va_arg(arg, int)); //char類型提升,用int類型。 break; case 's': { /*puts(va_arg(arg, char*));*/ //字符串可以直接用puts()函數輸出 char *ch = va_arg(arg, char*);//定義一個指針變量接收獲取的字符,用putchar()一個一個輸出 while (*ch) { putchar(*ch); ch++; } } break; case 'f':{ float a=(float)va_arg(arg,double); //float類型提升,所以用double (小陷阱) printf("%f",a); //BUG 要模擬浮點型比較復雜,這里耍個小聰明~ } break; default : break; } } else { putchar(*start) ; } start++; } va_end(arg); //必須有這一步,結束棧操作 return 0; } int main() { char str[]="Beat box!"; My_print("Output:>%f %c%c %d %s",3.14,'t','p',1234,str) ; return 0; }
結果:

注意陷阱
從上面的例子中,不難注意到了這樣一個問題,這里借助查詢的資料來說明:
我們用va_arg(ap,type)取出一個參數的時候,
type絕對不能為以下類型:
——char、signed char、unsigned char
——short、unsigned short
——signed short、short int、signed short int、unsigned short int
——float
在沒有函數原型的情況下,char與short類型都將被默認轉換為int類型,float類型將被轉換為double類型。
——《C語言程序設計》第2版 2.7 類型轉換 p36
va_arg宏的第2個參數不能被指定為char、short或者float類型。
因為char和short類型的參數會被轉換為int類型,而float類型的參數會被轉換為double類型 ……
例如,這樣寫肯定是不對的:
c = va_arg(ap,char);
因為我們無法傳遞一個char類型參數,如果傳遞了,它將會被自動轉化為int類型。上面的式子應該寫成:
c = va_arg(ap,int);
——《c陷阱與缺陷》
這就是在實現可變參數時,常常需要注意的小問題
至於前面所說的那兩個宏
_ADDRESSOF(V)
源碼:#define _ADDRESSOF(v) ( &(v) )
其實就是對變量v取地址的意思。
_INTSIZEOF(v)
源碼:#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
按式子可以先把這個宏改寫成:(sizeof(n) + 4 - 1)& (-3) 然后傳入變量 n,
n如占2個字節,就成了 5&(111...100)=4;
如占3個字節,就成了6&(111...100)=4;
如占5個字節,就成了8&(111...100)=8;
。。。
結果始終是4的倍數,如此便不難發現上面所述的規律了。
如有錯誤,希望指出!
歡迎來擾~~

