您的位置:首頁>正文

C語言大神之作可變參數函數的實現,能長能短,你也可以做到

C函式呼叫的棧結構

可變參數函數的實現與函式呼叫的棧結構密切相關, 正常情況下C的函數參數入棧規則為__stdcall, 它是從右到左的,

即函數中的最右邊的參數最先入棧。 例如, 對於函數:

學習資料, 專業群解答, 免費直播課程, ”C/C++ 8群 491994603

其棧結構為

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位元組)

因此, 函數的所有參數是存儲在線性連續的棧空間中的, 基於這種存儲結構, 這樣就可以從可變參數函數中必須有的第一個普通參數來定址後續的所有可變參數的類型及其值。

先看看固定參數清單函數:

學習資料, 專業群解答, 免費直播課程, ”C/C++ 8群 491994603

對於固定參數清單的函數, 每個參數的名稱、類型都是直接可見的, 他們的地址也都是可以直接得到的, 比如:通過&a我們可以得到a的位址, 並通過函數原型聲明瞭解到a是int類型的。

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

學習資料, 專業群解答, 免費直播課程, ”C/C++ 8群 491994603

這裡我們只能得到fmt這固定參數的位址,

僅從函數原型我們是無法確定"..."中有幾個參數、參數都是什麼類型的。 回想一下函數傳參的過程, 無論"..."中有多少個參數、每個參數是什麼類型的, 它們都和固定參數的傳參過程是一樣的, 簡單來講都是棧操作, 而棧這個東西對我們是開放的。 這樣一來, 一旦我們知道某函數幀的棧上的一個固定參數的位置, 我們完全有可能推導出其他變長參數的位置。

我們先用上面的那個fixed_args_func函數確定一下入棧順序。

學習資料, 專業群解答, 免費直播課程, ”C/C++ 8群 491994603

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

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

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); 根據這一結論我們試著實現一個支援可變參數的函數:

學習資料, 專業群解答, 免費直播課程, ”C/C++ 8群 491994603

期待輸出結果:

4

5

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", ap),而是printf("%s", tmp_ptr); printf("%s", ap)是意圖將ap所指的區塊作為字串輸出了,但是ap -> &tmp_ptr,tmp_ptr所佔據的4個位元組顯然不是字串,而是一個位址。如何讓&tmp_ptr是char **類型的,我們將ap進行強制轉換(char**)ap <=> &tmp_ptr,這樣我們訪問tmp_ptr只需要在(char**)ap前面加上一個*即可,即printf("%s", *(char**)ap);

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

改正後的代碼如下:

學習資料,專業群解答,免費直播課程,”C/C++ 8群 491994603

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

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

學習資料,專業群解答,免費直播課程,”C/C++ 8群 491994603

對比一下 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語言學習分享就到這裡結束了,感謝大家的觀看。

學習資料,專業群解答,免費直播課程,”C/C++ 8群 491994603

我們用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", ap),而是printf("%s", tmp_ptr); printf("%s", ap)是意圖將ap所指的區塊作為字串輸出了,但是ap -> &tmp_ptr,tmp_ptr所佔據的4個位元組顯然不是字串,而是一個位址。如何讓&tmp_ptr是char **類型的,我們將ap進行強制轉換(char**)ap <=> &tmp_ptr,這樣我們訪問tmp_ptr只需要在(char**)ap前面加上一個*即可,即printf("%s", *(char**)ap);

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

改正後的代碼如下:

學習資料,專業群解答,免費直播課程,”C/C++ 8群 491994603

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

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

學習資料,專業群解答,免費直播課程,”C/C++ 8群 491994603

對比一下 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語言學習分享就到這裡結束了,感謝大家的觀看。

學習資料,專業群解答,免費直播課程,”C/C++ 8群 491994603

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示