變參函式實現細節

2021-05-23 23:00:46 字數 2891 閱讀 4092

c語言的函式雖然不具備c++的多型性,但也可以接受引數不確定的情況,當然,c語言中的變參函式實際在功能上是受限的,廢話不多講,下面來看看變參函式的邊邊角角的問題。

討論之前我們來看一下最熟悉的變參函式printf的原型宣告:

int printf(const char *format,  ...);

注意到,在函式中宣告其引數是可變的方法是三個點「...」,但同時,這個函式必須要有乙個固定的引數,比如printf裡面的這個format,也就是說變參函式的引數數目至少是乙個。這是由c語言中實現變參的原理---計算堆疊位址---決定的。順著printf函式我們來看看它的定義是什麼:

int __printf(const char *format, ...)

(注意到庫函式中內部定義的變數和函式用了雙下劃線開頭,這也是我們寫應用程式時盡量不要用雙下劃線開頭的原因,我們也不應該使用單下劃線開頭的函式和變數,因為那也是系統保留的)

其中發現__printf函式裡用了va_list,va_start,va_end等巨集,事實上,在__printf中呼叫的vfpirntf函式還用到了乙個叫做va_arg的巨集,這幾個巨集就是編寫變參函式的關鍵。現在我們自己寫乙個最簡單的變參函式,先來個感性認識:

#include

#include

void ******_va_fun(int i, ...)

int main(void)

如**中的注釋所示,arg_ptr實際上是乙個指向函式變參列表的指標,va_list實際上是void指標型別。

va_start用來初始化這個指標,使之指向變參列表中的第乙個引數,注意到它的第乙個引數是函式中的固定引數,第二個引數是這個固定引數的型別。

va_arg利用已經初始化了的arg_ptr指標來取得變參列表中各個引數的值,第乙個引數是變參列表指標,第二個引數是當前引數的型別。

va_end巨集用來提示結束引數結束,在linux的glibc實現中,va_end實際上就是乙個空語句(void)0

各個巨集定義在標頭檔案stdarg.h中宣告,因此我們需要包含這個標頭檔案。其具體的定義如下:

#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是很笨的,它是不知道的。

用va list實現變參函式

va list 是c語言中解決變參問題的一組巨集。1.api介紹 標頭檔案 include下面是實現變參函式的一組巨集 macro void va start va list ap,last type va arg va list ap,type void va end va list ap voi...

變參函式設計

變參函式的應用得比較少.輸出log資訊功能的函式演常會用到變參函式.什麼是變參函式?比如 printf 個數 d,總數 d a,b printf就是變參函式,因為其引數的個數不是固定的.變參函式一般用到下列巨集 va arg retrieve argument from list va end re...

變參函式剖析

變參函式定義形式如func type a,要求至少乙個固定引數,因為需要通過這個引數來確定究竟有多少個引數 以及引數的型別。windows中,變參函式用來獲取引數的幾個巨集定義如下。typedef char va list define addressof v v define intsizeof ...