- 相關推薦
C語言中變參函數的實現細節
C語言的函數雖然不具備C++的多態性,但也可以接受參數不確定的情況,當然,C語言中的變參函數實際在功能上是受限的,廢話不多講,下面來看看變參函數的邊邊角角的問題。以下僅供參考!
討論之前我們來看一下最熟悉的變參函數printf的原型聲明:
--------------------------------------------------------------------------------
1 | int printf( const char *format, ...); |
--------------------------------------------------------------------------------
注意到,在函數中聲明其參數是可變的方法是三個點“...”,但同時,這個函數必須要有一個固定的參數,比如printf里面的這個format,也就是說變參函數的參數數目至少是一個。這是由C語言中實現變參的原理---計算堆棧地址---決定的。順著printf函數我們來看看它的定義是什么:
--------------------------------------------------------------------------------
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int __printf( const char *format, ...) { va_list arg; int done; va_start(arg, format); done = vfprintf(stdout, format, arg); va_end(arg); return done; } |
--------------------------------------------------------------------------------
(注意到庫函數中內部定義的變量和函數用了雙下劃線開頭,這也是我們寫應用程序時盡量不要用雙下劃線開頭的原因,我們也不應該使用單下劃線開頭的函數和變量,因為那也是系統保留的)
其中發現__printf函數里用了va_list,va_start,va_end等宏,事實上,在__printf中調用的vfpirntf函數還用到了一個叫做va_arg的宏,這幾個宏就是編寫變參函數的關鍵。現在我們自己寫一個最簡單的變參函數,先來個感性認識:
--------------------------------------------------------------------------------
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #include #include void simple_va_fun( int i, ...) { va_list arg_ptr; //定義一個用來指向函數變參列表的指針arg_ptr int j; va_start(arg_ptr, i); //使arg_ptr指向第一個可變參數 j = va_arg(arg_ptr, int ); //取得arg_ptr當前所指向的參數的值,并使arg_ptr指向下一個參數 va_end(arg_ptr); //指示提取參數結束 printf( "%d %d
" , i, j); return ; } int main( void ) { simple_va_fun( 3 , 4 ); return 0 ; } |
--------------------------------------------------------------------------------
如代碼中的注釋所示,arg_ptr實際上是一個指向函數變參列表的指針,va_list實際上是void指針類型。
va_start用來初始化這個指針,使之指向變參列表中的第一個參數,注意到它的第二個參數是變參函數的那個固定參數。
va_arg利用已經初始化了的arg_ptr指針來取得變參列表中各個參數的值,第一個參數是變參列表指針,第二個參數是當前參數的類型。
va_end宏用來提示結束參數結束,在LINUX的glibc實現中,va_end實際上就是一個空語句(void)0
各個宏定義在頭文件stdarg.h中聲明,因此我們需要包含這個頭文件。其具體的定義如下:
--------------------------------------------------------------------------------
1 2 3 4 5 6 7 8 9 10 11 | #define _AUPBND (sizeof(acpi_native_int) - 1 ) #define _ADNBND (sizeof(acpi_native_int) - 1 ) #define _bnd(X, bnd) (((sizeof(X)) + (bnd)) & (~(bnd))) #define va_start(ap, A) ( void )((ap) = ((( char *)&(A)) + (_bnd(A, _AUPBND))) #defind va_arg(ap, T) (*(T*)(((ap) += (_bnd(T, _AUPBND))) - (_bnd(T, _ADNBDN)))) #define va_end(ap) ( void ) 0 |
--------------------------------------------------------------------------------
這些宏定義都比較繁瑣,主要目的是為了適應不同系統的地址對齊問題。
上面說過,va_start的功能實際上是使ap指針指向第一個變參,A就是我們的第一個固定參數,不考慮地址對齊,最簡單的辦法當然如下:
ap = &A + sizeof(A)
上述代碼其實也是實現的這個簡單的功能,但經過宏_AUPBND和_bnd之后,就能保證ap指向的地址至少是關于acpi_native_int對齊的,打個比方,如果此時A的地址是0x0003,而且A的類型占用4個字節,而當前系統要求4個字節對齊,那么就讓_AUPBND中的sizeof參數為4,經過多次宏替代之后ap的地址值就會是0x0008,而簡單地用上面的算式ap = &A + sizeof(A)計算出的結果是0x0007。
同樣地,va_arg宏替代在不考慮任何移植性問題時,要取得當前變參的值并使指針指向下一個參數最簡單的辦法如下:
*((ap+=sizeof(T)) - sizeof(T))
這個需要稍微解釋一下,首先,C里面的參數壓棧是從右到左順序壓棧的,因此可以想象,第一個固定參數在棧頂(LINUX進程映像中棧是倒著增長的,這個地址是所有參數中最小的),第二個參數(也就是第一個變參)在緊接著固定參數之上,以此類推。因此,要想ap指針不斷指向下一個參數,就必須讓它每次都加上當前指向的變量所占內存的大小即 ap+=sizeof(T) 的含義。
接下來,利用這個地址值又減去sizeof(T),實際上地址值又回到上一個參數處(注意,此時ap指針的值并未改變,也就是說,va_arg宏實現獲取第一個變參的值的時候是先使ap指向第二個變參,然后再去獲取第一個變參的值),然后取值。
va_end宏就比較簡單了,雖然各種平臺的實現細節不一樣,但是道理都是一樣的,在glibc中va_end被簡單地實現為一個空語句。
由此可見,實際上C語言的所謂變參函數是很笨的,它基本上啥智能都沒有,不能跟C++的多態性和符號重載相比,我們在傳遞參數的時候雖然可以傳遞不定個數的參數,但是這些參數都必須在函數實現中給予一一處理。所以我還是比較推崇C++呵呵!
至于printf這個調皮鬼,上面看到它的原型了,里面還調用了vfprintf函數,這個函數就不分析了(實在太長了),它里面就用了va_arg來獲取各個變參的值。printf之所以可以識別各種變量類型,是因為你調用它的時候必須用printf修飾符,也就是%d,%f,%s等等來指定你的參數,printf是很笨的,它是不知道的。
【C語言中變參函數的實現細節】相關文章:
C語言中返回字符串函數的實現方法09-19
C語言中函數的區分08-30
c語言中time函數的用法08-27
C語言中gets()函數知識08-10
C語言中strpbr()函數的用法07-25
C語言中isalnum()函數和isalpha()函數的對比10-12
C語言中函數的區分有哪些10-25
在C語言中函數調用方式的區別09-01
C語言中strstr()函數的使用分析08-03
C語言中實現KMP算法實例08-09