c的多參設計原理

2021-07-30 07:44:15 字數 3373 閱讀 5605

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

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

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

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

int main()

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語言的庫函式 printf d d 1,2 的吧,使用確實很方便功能也很強大。但是為什麼它可以接受多個引數呢?現在我們來解析一下多參的實現原理,網上也找了一些文章。發現解析得都不全面。並且有bug。先看如下原始碼 include include include void myspr...

C語言中的變參原理

在c c 中,對函式引數的掃瞄是從後向前的。c c 的函式引數是通過壓入堆疊的方式來給函式傳引數的 堆疊是一種先進後出的資料結構 最先壓入的引數最後出來printf的第乙個被找到的引數就是那個字元指標,就是被雙引號括起來的那一部分,函式通過判斷字串裡控制引數的個數來判斷引數個數及資料型別,通過這些就...

C語言變參函式的實現原理

1.變參函式簡單示例 include include int accumlate int nr,va end arg return result int main 2.變參函式的實現原理 define va list void define va start arg,start arg va lis...