1. 程式人生 > >c語言中可變引數的實現

c語言中可變引數的實現

(一) 寫一個簡單的可變引數的C函式

下面我們來探討,如何寫一個簡單的可變引數的C函式。寫可變引數的C函式要在程式中用到以下這些巨集:
void va_start( va_list arg_ptr, prev_param );

type va_arg( va_list arg_ptr, type );

void va_end( va_list arg_ptr );
va在這裡是variable-argument(可變引數)的意思

這些巨集定義在stdarg.h中所以用到可變引數的程式應該包含這個標頭檔案。

下面我們寫一個簡單的可變引數的函式,該函式至少有一個整數引數,第二個引數也是整數,是可選的。函式只是列印這兩個引數的值。

void simple_va_fun(int i, ...)
{
va_list arg_ptr;
int j=0;

va_start(arg_ptr, i);
j=va_arg(arg_ptr, int);
va_end(arg_ptr);
printf("%d %d\n", i, j);
return;

}

我們可以在我們的標頭檔案中這樣宣告我們的函式:
extern void simple_va_fun(int i, ...);
我們在程式中可以這樣呼叫:
simple_va_fun(100);
simple_va_fun(100,200);
從這個函式的實現可以看到,我們使用可變引數應該有以下步驟:
1) 首先在函式裡定義一個va_list型的變數,這裡是arg_ptr,這個變數是指向引數的指標。
2) 然後用va_start巨集初始化變數arg_ptr
,這個巨集的第二個引數是可變引數函式的第一個引數,是一個固定的引數。
3) 然後用va_arg返回可變的引數,並賦值給整數j。 va_arg的第二個引數是你要返回的引數的型別,這裡是int型。
4) 最後用va_end巨集結束可變引數的獲取,然後你就可以在函式裡使用第二個引數了。如果函式有多個可變引數的,依次呼叫va_arg獲取各個引數。
如果我們用下面三種方法呼叫的話,都是合法的,但結果卻不一樣:
1) simple_va_fun(100);
結果是:100 -123456789(會變的值)
2) simple_va_fun(100,200);
結果是:100 200
3) simple_va_fun(100,200,300);
結果是:100 200

我們看到第一種呼叫有錯誤,第二種呼叫正確,第三種調用盡管結果正確,但和我們函式最初的設計有衝突。

下面一節我們探討出現這些結果的原因和可變引數在編譯器中是如何處理的。


(二)可變引數在編譯器中的處理

我們知道va_start,va_argva_end是在stdarg.h中被定義成巨集的,由於1)硬體平臺的不同 2)編譯器的不同,所以定義的巨集也有所不同,下面以VC++中stdarg.h裡x86平臺的巨集定義摘錄如下(’\’號表示折行):

typedef char * va_list;

#define _INTSIZEOF(n) \
((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )

#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )

#define va_arg(ap,t) \
( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

#define va_end(ap) ( ap = (va_list)0 )

定義_INTSIZEOF(n)主要是為了某些需要記憶體的對齊的系統。C語言的函式是從右向左壓入堆疊的,圖(1)是函式的引數在堆疊中的分佈位置我們看到va_list被定義成char*,有一些平臺或作業系統定義為void*。再看va_start的定義,定義為&v+_INTSIZEOF(v),而&v是固定引數在堆疊的地址,所以我們執行va_start(ap, v)以後,ap指向第一個可變引數在堆疊的地址,如圖:

高地址|-----------------------------|
|函式返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n個引數(第一個可變引數) |
|-----------------------------|<--va_start後ap指向
|第n-1個引數(最後一個固定引數)|
低地址|-----------------------------|<-- &v
圖( 1 )

然後我們用va_arg()取得型別t的可變引數值以上例為int型為例我們看一下va_arg取int型的返回值:
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
首先ap+=sizeof(int),已經指向下一個引數的地址了。然後返回ap-sizeof(int)的int*指標,這正是第一個可變引數在堆疊裡的地址

然後用*取得這個地址的內容(引數值)賦給j。


高地址|-----------------------------|
|函式返回地址 |
|-----------------------------|
|....... |
|-----------------------------|<--va_arg後ap指向
|第n個引數(第一個可變引數) |
|-----------------------------|<--va_start後ap指向
|第n-1個引數(最後一個固定引數)|
低地址|-----------------------------|<-- &v
圖( 2 )

最後要說的是va_end巨集的意思x86平臺定義為ap=(char*)0;使ap不再指向堆疊,而是跟NULL一樣。有些直接定義為((void*)0),這樣編譯器不會為va_end產生程式碼,例如gcc在linux的x86平臺就是這樣定義的
在這裡大家要注意一個問題:由於引數的地址用於va_start巨集,所以引數不能宣告為暫存器變數或作為函式或陣列型別。
關於va_start, va_arg, va_end的描述就是這些了,我們要注意的是不同的作業系統和硬體平臺的定義有些不同,但原理卻是相似的。

(三)可變引數在程式設計中要注意的問題

因為va_start, va_arg, va_end等定義成巨集,所以它顯得很愚蠢。

可變引數的型別和個數完全在該函式中由程式程式碼控制,它並不能智地識別不同引數的個數和型別。

有人會問:那麼printf中不是實現了智慧識別引數嗎?那是因為函式printf是從固定引數format字串來分析出引數的型別,再呼叫va_arg的來獲取可變引數的。也就是說你想實現智慧識別可變引數的話是要通過在自己的程式裡作判斷來實現的。

另外有一個問題,因為編譯器對可變引數的函式的原型檢查不夠嚴格,對程式設計查錯不利。

如果simple_va_fun()改為:

void simple_va_fun(int i, ...)
{
va_list arg_ptr;
char *s=NULL;

va_start(arg_ptr, i);
s=va_arg(arg_ptr, char*);
va_end(arg_ptr);
printf("%d %s\n", i, s);
return;
}
可變引數為char*型,當我們忘記用兩個引數來呼叫該函式時,就會出現core dump(Unix) 或者頁面非法的錯誤(window平臺)。但也有可能不出錯,但錯誤卻是難以發現,不利於我們寫出高質量的程式。
以下提一下va系列巨集的相容性。
System V Unix把va_start定義為只有一個引數的巨集:
va_start(va_list arg_ptr);
而ANSI C則定義為:
va_start(va_list arg_ptr, prev_param);
如果我們要用system V的定義,應該用vararg.h標頭檔案中所定義的巨集,ANSI C的巨集跟system V的巨集是不相容的,我們一般都用ANSI C,所以
用ANSI C的定義就夠了,也便於程式的移植。