可變參數模擬printf()函數實現一個my_print()函數以及調用可變參數需注意的陷阱

入棧規則


可變參數函數的實現與函數調用的棧幀結構是密切相關的。因此在咱們實現可變參數以前,先得搞清楚 棧是怎樣傳參的。函數

正常狀況下,C的函數參數入棧遵守__stdcall規則, 它是從右到左的,即函數中的參數入棧是從右到左的spa

例如:設計

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 }

從各個形參變量的地址能夠看出它們地址大小確實是從右到左依次減少的,說明它們是從右到左壓棧的,3d

 

實現原理


對於固定參數列表的函數,每一個參數的名稱、類型都是直接可見的,他們的地址也都是能夠直接獲得的,好比:經過&a就能夠獲得a的地址,並經過函數原型聲明瞭解到a是char類型的。指針

   對於變長參數的函數,怎麼辦呢?其實想一想函數傳參的過程,不管"..."中有多少個參數、每一個參數是什麼類型的,它們都和固定參數的傳參過程是同樣的,簡單來說都是棧操做,同時C標準的說明中,是支持變長參數的函數在原型聲明中的,但須至少有一個最左固定參數,嘿嘿~ 這樣,咱們不就能夠獲得其中固定參數的地址了嗎?code

  知道了某函數幀的棧上的一個固定參數的位置,因此咱們徹底能夠本身經過棧操做,推導出其餘變長參數的位置,進而實現可變參數函數。(這個「固定的參數」通常就是可變參數函數裏在第一個位置的參數,經過它就能夠開始找到後面各類類型、個數不定的參數了)orm

上述說了一下實現原理,知道的大佬就請忽略咯~blog

 

實現步驟


咱們經常使用的可變參數列表有這幾個:
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的倍數,如此便不難發現上面所述的規律了。

 

若有錯誤,但願指出!

   歡迎來擾~~

相關文章
相關標籤/搜索