C語言中可變引數函式實現原理

2022-02-08 04:02:14 字數 4030 閱讀 5154

c函式呼叫的棧結構

可變引數函式的實現與函式呼叫的棧結構密切相關,正常情況下c的函式引數入棧規則為__stdcall, 它是從右到左的,即函式中的最右邊的引數最先入棧。例如,對於函式:

void fun(int a, int b, int

c)

其棧結構為

0x1ffc-->d

0x2000-->a

0x2004-->b

0x2008-->c

對於在32位系統的多數編譯器,每個棧單元的大小都是sizeof(int), 而函式的每個引數都至少要佔乙個棧單元大小,如函式 void fun1(char a, int b, double c, short d) 對乙個32的系統其棧的結構就是

0x1ffc-->a  (4位元組)(為了字對齊)

0x2000-->b  (4位元組)

0x2004-->c  (8位元組)

0x200c-->d  (4位元組)

先看看固定引數列表函式:

void fixed_args_func(int a, double b, char *c)

對於固定引數列表的函式,每個引數的名稱、型別都是直接可見的,他們的位址也都是可以直接得到的,比如:通過&a我們可以得到a的位址,並通過函式原型宣告了解到a是int型別的。

但是對於變長引數的函式,我們就沒有這麼順利了。還好,按照c標準的說明,支援變長引數的函式在原型宣告中,必須有至少乙個最左固定引數(這一點與傳統c有區別,傳統c允許不帶任何固定引數的純變長引數函式),這樣我們可以得到其中固定引數的位址,但是依然無法從宣告中得到其他變長引數的位址,比如:

void var_args_func(const

char *fmt, ...)

這裡我們只能得到fmt這固定引數的位址,僅從函式原型我們是無法確定"..."中有幾個引數、引數都是什麼型別的。回想一下函式傳參的過程,無論"..."中有多少個引數、每個引數是什麼型別的,它們都和固定引數的傳參過程是一樣的,簡單來講都是棧操作

,而棧這個東西對我們是開放的。這樣一來,一旦我們知道某函式幀的棧上的乙個固定引數的位置,我們完全有可能推導出其他變長引數的位置。

我們先用上面的那個

fixed_args_func

函式確定一下入棧順序。

int

main()

a = 0x0022ff50

b = 0x0022ff54

c = 0x0022ff5c

從這個結果來看,顯然引數是從右到左,逐一壓入棧中的(棧的延伸方向是從高位址到低位址,棧底的占領著最高記憶體位址,先入棧的引數,其地理位置也就最高了)。

我們基本可以得出這樣乙個結論:

c.addr = b.addr + x_sizeof(b);  /*

注意: x_sizeof !=sizeof

*/b.addr = a.addr + x_sizeof(a);

有了以上的"等式",我們似乎可以推導出 void var_args_func(const char * fmt, ... ) 函式中,可變引數的位置了。起碼第乙個可變引數的位置應該是:first_vararg.addr = fmt.addr + x_sizeof(fmt);  根據這一結論我們試著實現乙個支援可變引數的函式:

#include 

#include

void var_args_func(const

char *fmt, ...)

intmain()

期待輸出結果:45

hello world

先來解釋一下這個程式。我們用ap獲取第乙個變參的位址,我們知道第乙個變參是4,乙個int 型,所以我們用(int*)ap以告訴編譯器,以ap為首位址的那塊記憶體我們要將之視為乙個整型來使用,*(int*)ap獲得該引數的值;接下來的變參是5,又乙個int型,其位址是ap + sizeof(第乙個變參),也就是ap + sizeof(int),同樣我們使用*(int*)ap獲得該引數的值;最後的乙個引數是乙個字串,也就是char*,與前兩個int型引數不同的是,經過ap + sizeof(int)後,ap指向棧上乙個char*型別的記憶體塊(我們暫且稱之tmp_ptr, char *tmp_ptr)的首位址,即ap -> &tmp_ptr,而我們要輸出的不是printf("%s\n", ap),而是printf("%s\n", tmp_ptr); printf("%s\n", ap)是意圖將ap所指的記憶體塊作為字串輸出了,但是ap -> &tmp_ptr,tmp_ptr所佔據的4個位元組顯然不是字串,而是乙個位址。如何讓&tmp_ptr是char **型別的,我

們將ap進行強制轉換(char**)ap <=> &tmp_ptr,這樣我們訪問tmp_ptr只需要在(char**)ap前面加上乙個*即可,即printf("%s\n",  *(char**)ap);

一切似乎很完美,編譯也很順利通過,但執行上面的**後,不但得不到預期的結果,反而整個編譯器會強行關閉(大家可以嘗試著執行一下),原來是ap指標在後來並沒有按照預期的要求指向第二個變引數,即並沒有指向5所在的首位址,而是指向了未知記憶體區域,所以編譯器會強行關閉。其實錯誤開始於:ap =  ap + sizeof(int);由於記憶體對齊,編譯器在棧上壓入引數時,不是乙個緊挨著另乙個的,編譯器會根據變參的型別將其放到滿足型別對齊的位址上的,這樣棧上引數之間實際上可能會是有空隙的。(

c語言記憶體對齊詳解(1)

c語言記憶體對齊詳解(2)

c語言記憶體對齊詳解(3)

)所以此時的ap計算應該改為:ap =  (char *)ap +sizeof(int) + __va_rounded_size(int);

改正後的**如下:

#include#define __va_rounded_size(type)  \(((

sizeof (type) + sizeof (int) - 1) / sizeof (int)) * sizeof (int

))void var_args_func(const

char *fmt, ...)

intmain()

var_args_func只是為了演示,並未根據fmt訊息中的格式字串來判斷變參的個數和型別,而是直接在實現中寫死了。

為了滿足**的可移植性,c標準庫在stdarg.h中提供了諸多便利以供實現變長長度引數時使用。這裡也列出乙個簡單的例子,看看利用標準庫是如何支援變長引數的:

1 #include #include 2

3void std_vararg_func(const

char *fmt, ...)

1314

intmain()

對比一下std_vararg_func和var_args_func的實現,va_list似乎就是char*,va_start似乎就是((char*)&fmt) + sizeof(fmt),va_arg似乎就是得到下乙個引數的首位址。沒錯,多數平台下stdarg.h中va_list, va_start和var_arg的實現就是類似這樣的。一般stdarg.h會包含很多巨集,看起來比較複雜。

下面我們來**如何寫乙個簡單的可變引數的c 函式.

使用可變引數應該有以下步驟:

1)首先在函式裡定義乙個va_list型的變數,這裡是arg_ptr,這個變數是指向引數的指標.

2)然後用va_start巨集初始化變數arg_ptr,這個巨集的第二個引數是第乙個可變引數的前乙個引數,是乙個固定的引數.

3)然後用va_arg返回可變的引數,並賦值給整數j. va_arg的第二個引數是你要返回的引數的型別,這裡是int型.

4)最後用va_end巨集結束可變引數的獲取.然後你就可以在函式裡使用第二個引數了.如果函式有多個可變引數的,依次呼叫va_arg獲取各個引數.

在《c程式語言》中,ritchie提供了乙個簡易版printf函式:

1 #include2

3void minprintf(char *fmt, ...)416

switch(*++p) 33}

34va_end(ap);

35 }

C語言中可變引數函式實現原理

說的非常詳細,但是有部分口誤,希望只吸取精華 c函式呼叫的棧結構 可變引數函式的實現與函式呼叫的棧結構密切相關,正常情況下c的函式引數入棧規則為 stdcall,它是從右到左的,即函式中的最右邊的引數最先入棧。例如,對於函式 void fun int a,int b,int c 其棧結構為 0x1f...

實現c語言中的可變引數函式

c語言程式設計中有時會遇到一些引數個數可變的函式,例如printf 函式,其函式原型為 int printf const char format,它除了有乙個引數format固定以外,後面跟的引數的個數和型別是可變的 用三個點 做引數佔位符 實際呼叫時可以有以下的形式 printf d i prin...

C語言中可變引數函式的實現

c語言的可變引數函式的實現需要使用標頭檔案stdarg.h,在該標頭檔案中定義了乙個變數型別va list和三個巨集va start va arg va end 下面將在 中講解這幾個巨集的使用方法。第一種方法是在函式內部手動指定可變引數的型別。首先需要知道可變引數的個數,並作為第乙個引數傳入。由於...