1. 程式人生 > >C++不定引數的應用

C++不定引數的應用

不定引數當年做為C/C++語言一個特長被很多人推崇,但是實際上這種技術並沒有應用很多。除了格式化輸出之外,我實在沒看到多少應用。主要原因是這種技術比較麻煩,副作用也比較多,而一般情況下過載函式也足以替換它。儘管如此,既然大家對它比較感興趣,我就簡單總結一下它的使用和需要注意的常見問題。

原理

剛學C語言的時候,一般人都會首先接觸printf函式。通過這個函式,你可以列印不定個數的變數到螢幕,如:

printf("%d", 3);
printf("%d,%d",3,4);

 上述程式碼看似簡單,實際上卻需要我們解決許多問題。在我們設計printf的時候,我們是不知道到底會傳入幾個引數的。在這種未知的情況下,我們需要解決下面幾個問題:

  1. 怎麼告訴printf我們會傳入幾個引數
  2. printf怎麼去訪問這些引數
  3. 函式呼叫完成後,系統怎麼把引數從傳遞用的堆疊中釋放

為了解決這些問題,我們首先要解釋cdecl呼叫約定(參見論呼叫約定),所有使用不定引數的函式必須是使用cdecl(全域性函式)或者this call(類成員函式)呼叫約定。該約定對於引數傳遞規定如下:

  1. 引數從右向左入棧(也就是如果你呼叫f(a,b,c),則c先入棧,然後是b,最後是a入棧)
  2. 呼叫者負責清理堆疊

其中第二點直接解決了前面三個問題中的第三個問題。我們來詳細說說其他兩個問題。

確定引數的個數

在一個函式中,一般有如下prolog程式碼:

00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,48h
執行上述程式碼之後,func(a,b,c)函式所處的堆疊上下文就變成如下佈局:

堆疊佈局

其中,ebp指向儲存舊的ebp的堆疊記憶體的下一個字的地址,ebp+8指向eip地址,ebp+12則指向函式呼叫的第一個引數,而ebp和esp之間是用於臨時變數(也就是堆疊變數)的空間。

注意,由於上述prolog程式碼的存在,我們很容易通過ebp得到第一個引數的地址,對於不定引數列表之前的型別固定的引數,我們也可以根據型別資訊得到其實際的位置(例如,第一個引數的位置偏移第一個引數的大小,就是第二個引數的地址)。

注意不定引數函式有個限制,就是不定引數的列表必須在整個函式的引數列表的最後。我們不可以定義如下的函式:

void func(int a, ..., int c)

所有型別固定的引數都必須出現在引數列表的開始。這樣根據前面的論述,我們就可以得到所有型別固定的引數。

在設計具有不定引數列表的函式的時候,我們有兩種方法來確定到底多少引數會被傳遞進來。

方法1是在型別固定的引數中指明後面有多少個引數以及他們的型別。printf就是採用的這種方法,它的format引數指明後面每個引數的型別。

方法2是指定一個結束引數。這種情況一般是不定引數擁有同樣的型別,我們可以指定一個特定的值來表示引數列表結束。下面這個sum函式就是一個例子:

int sumi(int c, ...)
{
    va_list ap;
    va_start(ap,c);
    
int i;
    
int sum = c;
    c 
= va_arg(ap,int);
    
while(0!=c)
    
{
        sum 
= sum+c;
        c 
= va_arg(ap,int);
    }

    
return sum;
}

使用這個函式的程式碼為:

int main(int argc, char* argv[])
{
    
int i=sumi(1,2,3,4,5,6,7,8,9,0);
    
    
return0;
}

訪問各個引數

其實前文已經告訴我們怎麼去訪問不定引數。va_start和va_arg函式可以被結合起來用於依次訪問每個函式,他們實際上都是巨集函式。

在vc6,va_start函式定義為:

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )

其中_INTSIZEOF(n)計算比n大的sizeof(int)的最小倍數,如果n=101,則_INTSIZEOF(n)為104。

va_start執行完畢後,ap指向變數v後第一個4位元組對齊的地址。例如,v的地址為0x123456, v的大小為13,則v後面的下一個與字邊界對齊的地址為0x123456+0x0D=0x123463再調整為與4位元組對齊的下一個地址,也就是0x123464.

va_arg函式定義為:

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

分析與va_start一樣,它的結果是使ap指向當前變數的下一個變數。

這樣,我們只要在開始時使用va_start把不定引數列表賦值給ap,然後依次用va_arg獲得不同引數即可。

潛在問題

使用不定引數列表,有兩個問題特別需要注意。

問題1的理解相對簡單:我們在過載一個函式的時候,不能依賴不定引數列表部分對函式進行區分。

假定我們定義兩個過載函式如下:

int func(int a, int b, ...)

int func(int a, int b, float c);

則上述函式會導致編譯器不知道怎麼去解釋func(1,2, 3.3),因為當第三個引數為浮點數時,兩個實現都可以滿足匹配要求。一般情況,個人建議對於不定引數函式不要去做過載。

另外一個問題是關於型別問題。絕大多數情況下,C和C++的變數都是強型別的,而不定引數列表屬於一個特例。

當我們呼叫va_arg的時候,我們指明下一個引數的型別,而在執行的時候,va_arg正是根據這個資訊在堆疊上來找到對應的引數的。如果我們需要的型別和真實傳遞進來的引數完全一致時自然沒有問題,但是假如型別不一樣,則會有大麻煩。

假如上面的的sumi函式,我們用下面方法呼叫:

int sum = sumi(12.230)

注意第二個引數我們傳入了一個double型別的2.2,我們希望sumi在做加法時可以做隱式型別轉換,轉換為int進行計算。但是實際情況時,當我們分析到這個引數時,呼叫的是:

c=va_arg(ap,int)

根據前文va_arg的定義,這個巨集被翻譯成:

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

如果後面的+=計算出正確的地址,最後就變成

*(int*)addr

如果希望能得到正確的整數值,必須要求addr所在的地址是一個真實的int型別。但是當我們傳入double時,實際上其記憶體佈局和int完全不同,因此我們得不到需要的整數。感興趣的朋友可以用下面簡單的程式碼做測試:

double a;
a
=1.1;
int b =*(int*& a;

因此,當我們呼叫有不定引數列表的函式時,不要期望系統做隱式型別轉換,系統不會做這種檢查或者轉換,你給的引數型別必須嚴格和你希望的值一樣。