用指標處理 C 語言中不定數目的函數參數 現在我們每編一個程式幾乎都會用到兩個函數-printf和scanf。發現這兩個函數和普通函數的不同之處了嗎?那就是這兩個函數都可以處理不定數目的實參。C語言是一種很寬鬆的語言,它甚至允許 程式員 對函數傳
用指標處理C語言中不定數目的函數參數
現在我們每編一個程式幾乎都會用到兩個函數-printf和scanf。發現這兩個函數和普通函數的不同之處了嗎?那就是這兩個函數都可以處理不定數目的實參。C語言是一種很寬鬆的語言,它甚至允許程式員對函數傳遞任意數目的參數。而這個特性在某些情況下是非常有用的。比如,現在我們要編一個求一系列整數平均值的函數average(),如果不用變長度實參,可能需要定義以下一系列原型:
int average_1 ( int );
int average_2 ( int, int );
int average_3 ( int, int, int );
…
雖然C++的重載功能可以使這些函數變成同名的,但是編碼的工作量會變得很大,而且程式會變得沒有美感,況且這種類型的函數有無窮多種,怎麼定義得過來呢?現在,我們用變長度實參,可以唯寫一個函數,就能處理所有的情況。函數原型如下:
int average ( int, … );
注意參數列表中的三個點是符合C語言的文法的,而不是我省略了什麼東西。這三個點出現在函數形式參數列表的最後,表示這以後可以向函數傳遞任意數目的實參。例如,函數scanf的聲明如下:
int __cdecl scanf (const char *format,…);
注:__cdecl聲明的是參數傳遞模式,因為__cdecl是預設的,因此可以省略.
注意,三個點必須出現在參數列表的最後,而且前面必須要定義至少一個的形式參數。現在我們可以通過函數調用average(0,1,2,3,4,5,-1);來求0到5的平均值了(最後一個-1是結束標誌,不參與求值運算,這一點在後面會講到)。
現在的問題是,雖然我們順利地聲明了函數原型,並且也順利地調用了函數,但是怎樣在函數中接受這些參數呢?不幸的是C語言本身並沒有提供像聲明參數那樣簡單的接受參數的機制,顯然,我們不可能只用一個省略符號來接受這些參數。但是我們可以用指標來解決這個問題(用嵌入式彙編也可以解決這個問題,但是那超出了C語言的範疇)。先讓我們來看一下C語言函數傳遞參數的機制。
C語言中的函數一般①是通過把參數拷貝到一塊特殊的記憶體地區(稱為堆棧)內傳遞給函數的。比如,函數調用
scanf( “%d %f %c %C %s %S”, &i, &fp, &c, &wc, s, ws );
它的參數在記憶體中的樣子是:
地址
|
參數
|
……
|
……
|
0x0012f308
|
format
|
0x0012f30c
|
&i
|
0x0012f310
|
&fp
|
0x0012f314
|
&c
|
0x0012f318
|
&wc
|
0x0012f31c
|
S
|
0x0012f320
|
ws
|
……
|
……
|
(*)format是一個指向字串“%d %f %c %C %s %S”的指標
注:這裡討論的情況只對c語言預設的參數傳遞模式__cdecl成立,而不適用於__fastcall和__stdcall兩種模式.現在我們在寫的函數全都是__cdecl模式的。
而 format的地址我們可以用&format得到,所以以後各個參數的地址就可以通過增加指標&format的值來得到了。例如,&c的地址是p= (char**)((char*)&format+sizeof(char**)+sizeof(int**)+sizeof(float**))/*這 個式子夠煩的:-)*/,而&c的值就是*p了。因為C語言沒有提供關於參數數目的資訊,所以這個資訊要有程式員傳遞一個參數來實現。“%d %f %c %C %s %S”就是告訴scanf,它後面還有六個各種類型的指標作為參數。上面對函數average傳遞的最後一個參數-1也是用來作為結束標誌的。
說 到這兒,有一個問題我們不得不提,就是我們又一次遇到了對齊(alignment)問題。所謂對齊,對Intel80x86機器來說就是要求每個變數的地 址都是sizeof(int)的倍數。在32位(4位元組)機器上表現為所有的變數地址都能被4整除。這樣,變數在記憶體中就不一定是緊密排列的了。例如,下 面的函數:
int Bad_Example ( char c, int i );
如果參數c的地址是0x0012f308的話,i的地址就不是0x0012f309,而是被移到了0x0012f30c。一般的,如果參數type p1的地址是&p1的話,那麼它的後繼參數的地址可以通過在他後面加上一個位移
( char * ) & p1 + ( sizeof ( type ) + sizeof( int ) –1 ) /sizeof ( int ) * sizeof ( int )
得到。(注意,在加的時侯一定要先把&p1的類型轉化為(char*)。)
在標頭檔<stdarg.h>中定義了宏—INTSIZEOF()來得到這個位移值:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
這裡為了加快運算用了位操作,但功能是一樣的。現在,假設這個宏已經定義了,我們來實現average函數作為一個例子:
int average ( int n, ... )
{
int sum = 0, c = 0;
int *p = &n;
if ( n < 0 ) return 0;
while ( *p >= 0 )
{
sum += *p;
c++;
( char * ) p += _INTSIZEOF(int);
};
return sum/c;
}
由於這種操作具有通用性,所以ANSI C提供了三個宏來實現這種處理。這組宏定義在標頭檔<stdarg.h>中。
type va_arg ( va_list arg_ptr, type );
void va_end( va_list arg_ptr );
void va_start( va_list arg_ptr, prev_param );
操作方法如下:
先定義一個va_list類型的變數,然後用宏va_start給他賦初值,prev_param用省略符號前的參數名代替。然後用宏va_arg來挨個取得參數的值,參數的類型在type中指定。最後用宏va_end釋放變數。
下面是函數average的另一種實現方式:
int average ( int n, ... )
{
int sum = n, count = 1, p;
va_list arg_ptr;
if ( n < 0 ) return 0;
va_start( arg_ptr, n );
while( ( p = va_arg( arg_ptr, int ) ) >= 0 )
{
sum += p;
count++;
}
va_end( arg_ptr );
return sum/count;
}
最後還需要說明兩點。首先,上面講的內容與C 語言的實現有關,而C語言的實現又依賴於cpu和作業系統,所以並不適用於所有的電腦,而且隨機器的不同會由很大的區別。在<stdarg.h>內會看 到大量的條件編譯就是這個原因。其次,雖然函數定義中使用可變參數列表提供了很大的靈活性,但是對可變部分的參數C語言編譯器不會進行類型檢查,所以程式 中要特別小心,確保參數的傳遞和接受是正確的。
注:
① C語言中還有一種參數傳遞方式稱為__fastcall方式。這種方式是通過盡量把參數放入寄存器內來傳遞的(因為寄存器數目有限,用完後剩下的參數只能放入記憶體了),所以用這種方式傳遞參數會提高程式的效率