程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C語言可變參數函數實現原理

C語言可變參數函數實現原理

編輯:關於C語言

一、可變參數函數實現原理

C函數調用的棧結構:

可變參數函數的實現與函數調用的棧結構密切相關,正常情況下C的函數參數入棧規則為__stdcall, 它是從右到左的,即函數中的最右邊的參數最先入棧。

本文地址:http://www.cnblogs.com/archimedes/p/variable-parameter.html,轉載請注明源地址。

例如,對於函數:

 fun( a,  b, 

    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字節)

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

先看看固定參數列表函數:

 fixed_args_func( a,  b,  *, &, &, &

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

 var_args_func(  *

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

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

, ,  = = = 

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

 c.addr = b.addr + x_sizeof(b);  = a.addr + x_sizeof(a);

View Code

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\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語言內存對齊),所以此時的ap計算應該改為:ap =  (char *)ap +sizeof(int) + __va_rounded_size(int);

改正後的代碼如下:

View Code

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

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

View Code

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved