C語言可變參函式的實現

2021-07-10 04:15:25 字數 3744 閱讀 8711

函式是大多數程式語言都實現的程式設計要素,呼叫函式的實現原理就是:執行跳轉+引數傳遞。對於執行跳轉,所有的cpu都直接提供跳轉指令;對於引數傳遞,cpu會提供多種方式,最常見的方式就是利用棧來傳遞引數。c語言標準實現了函式呼叫,但是卻沒有限定實現細節,不同的c編譯器廠商可以根據底層硬體環境自行確定實現方式。

函式呼叫的一般實現原理,請參考我的博文c語言中利用setjmp和longjmp做異常處理中的第一段。

我們以x86架構上的vc++編譯器為例進行舉例說明。例子**如下。

void f(int x, int y, int z)

int main()

可能的執行結果:

00fff674, 00fff678, 00fff67c
vc++中函式的引數是通過堆疊傳遞的,引數按照從右向左的順序入棧。呼叫f時引數在堆疊中的情況如下圖所示:

可見,我們只要知道x的位址,就可以推算出y,z的位址,從而通過其位址取得引數y,z的值,而不用其引數名稱取值。如下**所示。

void f(int x, int y, int z)

int main()

可見根據函式的第乙個引數,以及後續引數的型別,就可以根據偏移量計算出後續引數的位址,從而取得後續引數值。

於是可以把上述**改寫成可變引數的形式。

void f(int x, ...)

int main()

雖然寫成了可變參形式,但是函式如何判斷後續實參的個數和型別呢?這就需要在固定引數中攜帶這些資訊,如printf(char*, …)使用的格式化字串方法,通過第乙個引數來攜帶後續引數個數以及型別的資訊。我們實現乙個簡單點的,只能識別%s,%d,%f三種標誌。

void f(char* fmt, ...)

else

if (*p == '%' && *(p+1) == 'f')

else

if (*p == '%' && *(p+1) == 's')

p++;

}}int main()

輸出:

引數型別為int,值為 100

引數型別為double,值為 1.230000

引數型別為char*,值為 hello world

為簡化分析引數**,定義一些巨集來簡化,如下。

#define va_list char*   /* 可變引數位址 */

#define va_start(ap, x) ap=(char*)&x+sizeof(x) /* 初始化指標指向第乙個可變引數 */

#define va_arg(ap, t) (ap+=sizeof(t),*((t*)(ap-sizeof(t)))) /* 取得引數值,同時移動指標指向後續引數 */

#define va_end(ap) ap=0 /* 結束引數處理 */

void f(char* fmt, ...)

else

if (*p == '%' && *(p+1) == 'f')

else

if (*p == '%' && *(p+1) == 's')

p++;

}va_end(ap);

}int main()

上面的例子中,我們沒有使用任何庫函式就輕鬆實現了可變引數函式。別高興太早,上述**在x86平台的vc++編譯器下可以順利編譯、正確執行。但是在gcc編譯後,執行卻是錯誤的。可見gcc對於可變引數的實參傳遞實現與vc++並不相同。

gcc下編譯執行:

[smstong@cf-19 ~]$ ./a.out

引數型別為int,值為 0

引數型別為double,值為 0.000000

segmentation fault

可見,上述**是不可移植的。為了在使得可變參函式能夠跨平台、跨編譯器正確執行,必須使用c標準標頭檔案stdarg.h中定義的巨集,而不是我們自己定義的。(這些巨集的名字和作用與我們自己定義的巨集完全相同,這絕不是巧合!)每個不同的c編譯器所附帶的stdarg.h檔案中對這些巨集的定義都不相同。再次重申一下這幾個巨集的使用正規化:

va_list ap;

va_start(ap, 固定引數名); /* 根據最後乙個固定引數初始化 */

可變引數1型別 x1 = va_arg(ap, 可變引數型別1); /* 根據引數型別,取得第乙個可變引數值 */

可變引數2型別 x2 = va_arg(ap, 可變引數型別2); /* 根據引數型別,取得第二個可變引數值 */

...va_end(ap); /* 結束 */

這次,把我們自己的巨集定義去掉,換成#include

#include 

#include

void f(char* fmt, ...)

else

if (*p == '%' && *(p+1) == 'f')

else

if (*p == '%' && *(p+1) == 's')

p++;

}va_end(ap);

}int main()

**在vc++和gcc下均可以正確執行了。

也許在有些編譯器環境中,va_end(ap);確實沒有什麼作用,但是在其他編譯器中卻可能涉及到記憶體的**,切不可省略。

《c語言程式設計》中提到:

在沒有函式原型的情況下,char與short型別都將被轉換為int型別,float型別將被轉換為double型別。實際上,用...標識的可變引數總是會執行這種型別提公升。
引用《c陷阱與缺陷》裡的話:

**va_arg巨集的第2個引數不能被指定為char、short或者float型別**。

因為char和short型別的引數會被轉換為int型別,而float型別的引數會被轉換為double型別 ……

例如,這樣寫肯定是不對的:

c = va_arg(ap,char);

因為我們無法傳遞乙個char型別引數,如果傳遞了,它將會被自動轉化為int型別。上面的式子應該寫成:

c = va_arg(ap,int);

對於可變引數,編譯器無法進行任何檢查,只能靠呼叫者的自覺來保證正確。

可變引數必須靠固定引數來定位,所以函式中至少需要提供固定引數,f(固定引數,…)。

當然,也可以提供更多的固定引數,如f(固定引數1,固定引數2,…)。注意的是,當提供2個或以上固定引數時,va_start(ap, x)巨集中的x必須是最後乙個固定引數的名字(也就是緊鄰可變引數的那個固定引數)。

c++的函式過載特性,允許重複使用相同的名稱來定義函式,只要同名函式的引數(型別或數量)不同。例如,

void f(int x);

void f(int x, double d);

void f(char* s);

雖然源**中函式名字相同,其實編譯器處理後生成的是三個具有不同函式名的函式(名字改編name mangling)。雖然在使用上有些類似之處,但這顯然與c的可變引數函式完全不是乙個概念。

C語言的可變參函式

可變參函式,顧其名而思義,就是該函式的引數數目不固定,例如我們的格式化輸出函式 int printf const char fmt,這個函式的 引數就代表它是個可變參的函式。這個函式第乙個引數一定是乙個字串,通過字串中的 d s 等等來對應後面的引數數量,這樣以來就可以正確解析函式的引數。實現可變參...

可變參函式(my printf可變參函式的實現)

可變參函式 其引數列表的引數型別與個數可變,採用ansi標準形式時,引數個數可變的函式的原型宣告是 type funcname type para1,type para2,至少需要乙個普通的形式引數,後面的省略號不表示省略,而是函式原型的一部分,為引數佔位符,type是函式返回值和形式引數的型別 可...

可變參函式

int add int x,int main int add int x,int sum 0 char point char x for int i 0 iint add int x,可變參函式原型,該函式中帶有識別符號的引數 x記錄的是引數的個數,後面的數字是需要求和的數。x的作用是為了標誌出加數...