沒事的時候,想一下printf,居然發現都不知道是怎麼樣的大概的原理。找到一篇文章,不過也加上一些自己的理解,寫出此文。
原文地址: http://www.cnblogs.com/liyou_blog/archive/2010/09/01/1814663.html
ANSI C中庫函數printf的正確形式如下:
int printf(char *fmt, ...);
其中,省略符號表示參數表的數量和類型是可變的。省略符號只能出現在參數表的尾部。如何?這樣一個具有可變參數表的函數?在《The C programme Language》中實現了這樣一個樣本函數:
void minprintf(char *fmt, ...);
先看看它的實現方式,然後研究下它的實現原理,函數minprintf只處理格式字串和參數,格式轉換則通過調用函數printf實現。
代碼#include "stdarg.h"#include "stdio.h"void minprintf(char *fmt, ...){ va_list ap; //依次指向每個無名參數 //不要怕,va_list是一個宏,後面會說到,還有va_start, va_arg 和 va_end char *p, *sval; int ival; double dval; va_start(ap,fmt); //將ap指向第一個無名參數 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);}
上述代碼中標準標頭檔“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 其實是一個字元指標類型,代碼中聲明一個變數。該變數將依次引用各參數。代碼中聲明此類型的變數為ap, 即“參數指標”。宏va_start將ap初始化為指向第一個無名參數的指標。第一個參數是 ap 本身,第二個參數是在變參表前面緊挨著的一個變數,即“...”之前的那個參數;在使用ap之前,該宏必須被調用一次。參數表必須至少包括一個有名參數(即“char *fmt”),va_start將最後一個有名參數作為起點。
然後調用va_arg擷取參數,它的第一個參數是ap,第二個參數是要擷取的參數的指定類型,然後返回這個指定類型的值,並且把 ap 的位置指向變參表的下一個變數位置;
最後擷取所有的參數之後,我們有必要將這個 ap 指標關掉,以免發生問題,方法是調用 va_end,它將輸入的參數 ap 置為 NULL。
函數參數的傳遞原理
函數參數是以資料結構以棧的形式存取,從右至左依次入棧。參數存放在記憶體的棧空間中,在執行函數的時候,從最後一個開始入棧。棧底高地址,棧頂低地址,舉例如下:
對於函數 void foo(int x, float y, char z);
那麼,調用函數的時候,實參 char z 先進棧,然後是 float y,最後是 int x,因此在記憶體中變數的存放次序是 x->y->z(從低地址到高地址的順序),因此,只要知道任意一個變數的地址,並且知道其他變數的類型(這樣才能正確的實現指標移位元運算),可以順藤摸瓜找到其他的輸入變數。一段驗證性的範例程式碼如下,輸出會為1,2,3,4,
代碼void fun(int a, ...){ int i; int *temp = &a; temp++; for (i=0 ; i<a; ++i) { printf("%d ,",*temp); temp++; }}int main(){ int a = 1; int b = 2; int c = 3; int d = 4; fun(4,a,b,c,d); return 0;}
最後,利用可變參數表,實現一個實踐中使用的函數:
int max(int nNum, ...){va_list ap;va_start(ap, nNum);//初始化apint maxNum = va_arg(ap, int);//得到最大的第一個資料int num;for(int i = 1; i < nNum; i++)//{num = va_arg(ap, int); //得到下一個元素if(num > maxNum){maxNum = num;}}va_end(ap);return maxNum;}int main(){printf("%d", max(5, 20, 11, 23, 5, 10));}
程式運行輸出 :
23
注意:
由於宏定義來實現變長參數表,因此可能會出現這麼一些問題:
一. 由於是宏實現,不會對參數類型進行檢查,參數很容易發生隱式的類型轉換擷取一個值(譬如輸入一個float,
卻以int型去類型轉化),這樣做會出現一些未曾預計的結果。
二. 參數指標移動時容易出現越界訪問,超出函數棧幀的實際範圍,可能導致程式崩潰的運行時結果。