C++ 控制結構和函式(三)—— 函式II(Functions II)
引數按數值傳遞和按地址傳遞(Arguments passed by value and by reference)
到目前為止,我們看到的所有函式中,傳遞到函式中的引數全部是按數值傳遞的(by value)。也就是說,當我們呼叫一個帶有引數的函式時,我們傳遞到函式中的是變數的數值而不是變數本身。 例如,假設我們用下面的程式碼呼叫我們的第一個函式addition :
int x=5, y=3, z;z = addition ( x , y );
在這個例子裡我們呼叫函式addition 同時將x和y的值傳給它,即分別為5和3,而不是兩個變數:
這樣,當函式addition被呼叫時,它的變數a
但在某些情況下你可能需要在一個函式內控制一個函式以外的變數。要實現這種操作,我們必須使用按地址傳遞的引數(arguments passed by reference),就象下面例子中的函式duplicate:
// passing parameters by reference #include <iostream.h> void duplicate (int& a, int& b, int& c) { a*=2; b*=2; c*=2; } int main () { int x=1, y=3, z=7; duplicate (x, y, z); cout << "x=" << x << ", y=" << y << ", z=" << z; return 0; } | x=2, y=6, z=14 |
第一個應該注意的事項是在函式duplicate的宣告(declaration)中,每一個變數的型別後面跟了一個地址符ampersand sign (&),它的作用是指明變數是按地址傳遞的(by reference),而不是像通常一樣按數值傳遞的(by value)。
當按地址傳遞(pass by reference)一個變數的時候,我們是在傳遞這個變數本身,我們在函式中對變數所做的任何修改將會影響到函式外面被傳遞的變數。
用另一種方式來說,我們已經把變數a, b,c和呼叫函式時使用的引數(x, y和 z)聯絡起來了,因此如果我們在函式內對a 進行操作,函式外面的x 值也會改變。同樣,任何對b 的改變也會影響y,對c 的改變也會影響z>
這就是為什麼上面的程式中,主程式main中的三個變數x, y和z在呼叫函式duplicate 後列印結果顯示他們的值增加了一倍。
如果在宣告下面的函式:
void duplicate (int& a, int& b, int& c)
時,我們是按這樣宣告的:
void duplicate (int a, int b, int c)
也就是不寫地址符 ampersand (&),我們也就沒有將引數的地址傳遞給函式,而是傳遞了它們的值,因此,螢幕上顯示的輸出結果x, y ,z 的值將不會改變,仍是1,3,7。
這種用地址符 ampersand (&)來宣告按地址"by reference"傳遞引數的方式只是在C++中適用。在C 語言中,我們必須用指標(pointers)來做相同的操作。
按地址傳遞(Passing by reference)是一個使函式返回多個值的有效方法。例如,下面是一個函式,它可以返回第一個輸入引數的前一個和後一個數值。
// more than one returning value #include <iostream.h> void prevnext (int x, int& prev, int& next) { prev = x-1; next = x+1; } int main () { int x=100, y, z; prevnext (x, y, z); cout << "Previous=" << y << ", Next=" << z; return 0; } |
Previous=99, Next=101 |
引數的預設值(Default values in arguments)
當宣告一個函式的時候我們可以給每一個引數指定一個預設值。如果當函式被呼叫時沒有給出該引數的值,那麼這個預設值將被使用。指定引數預設值只需要在函式宣告時把一個數值賦給引數。如果函式被呼叫時沒有數值傳遞給該引數,那麼預設值將被使用。但如果有指定的數值傳遞給引數,那麼預設值將被指定的數值取代。例如:
// default values in functions #include <iostream.h> int divide (int a, int b=2) { int r; r=a/b; return (r); } int main () { cout << divide (12); cout << endl; cout << divide (20,4); return 0; } |
6 5 |
我們可以看到在程式中有兩次呼叫函式divide。第一次呼叫:
divide (12)
只有一個引數被指明,但函式divide允許有兩個引數。因此函式divide 假設第二個引數的值為2,因為我們已經定義了它為該引數預設的預設值(注意函式宣告中的int b=2)。因此這次函式呼叫的結果是 6 (12/2)。
在第二次呼叫中:
divide (20,4)
這裡有兩個引數,所以預設值 (int b=2) 被傳入的引數值4所取代,使得最後結果為 5 (20/4).
函式過載(Overloaded functions)
兩個不同的函式可以用同樣的名字,只要它們的參量(arguments)的原型(prototype)不同,也就是說你可以把同一個名字給多個函式,如果它們用不同數量的引數,或不同型別的引數。例如:
// overloaded function #include <iostream.h> int divide (int a, int b) { return (a/b); } float divide (float a, float b) { return (a/b); } int main () { int x=5,y=2; float n=5.0,m=2.0; cout << divide (x,y); cout << "\n"; cout << divide (n,m); cout << "\n"; return 0; } |
2 2.5 |
在這個例子裡,我們用同一個名字定義了兩個不同函式,當它們其中一個接受兩個整型(int)引數,另一個則接受兩個浮點型(float)引數。編譯器 (compiler)通過檢查傳入的引數的型別來確定是哪一個函式被呼叫。如果呼叫傳入的是兩個整數引數,那麼是原型定義中有兩個整型(int)參量的函式被呼叫,如果傳入的是兩個浮點數,那麼是原型定義中有兩個浮點型(float)參量的函式被呼叫。
為了簡單起見,這裡我們用的兩個函式的程式碼相同,但這並不是必須的。你可以讓兩個函式用同一個名字同時完成完全不同的操作。
Inline 函式(inline functions)
inline 指令可以被放在函式宣告之前,要求該函式必須在被呼叫的地方以程式碼形式被編譯。這相當於一個巨集定義(macro)。它的好處只對短小的函式有效,這種情況下因為避免了呼叫函式的一些常規操作的時間(overhead),如引數堆疊操作的時間,所以編譯結果的執行程式碼會更快一些。
它的宣告形式是:
inline type name ( arguments ... ) { instructions ... }
它的呼叫和其他的函式呼叫一樣。呼叫函式的時候並不需要寫關鍵字inline ,只有在函式宣告前需要寫。
遞迴(Recursivity)
遞迴(recursivity)指函式將被自己呼叫的特點。它對排序(sorting)和階乘(factorial)運算很有用。例如要獲得一個數字n的階乘,它的數學公式是:
n! = n * (n-1) * (n-2) * (n-3) ... * 1
更具體一些,5! (factorial of 5) 是:
5! = 5 * 4 * 3 * 2 * 1 = 120
而用一個遞迴函式來實現這個運算將如以下程式碼:
// factorial calculator #include <iostream.h> long factorial (long a){ if (a > 1) return (a * factorial (a-1)); else return (1); } int main () { long l; cout << "Type a number: "; cin >> l; cout << "!" << l << " = " << factorial (l); return 0; } |
Type a number: 9 !9 = 362880 |
注意我們在函式factorial中是怎樣呼叫它自己的,但只是在引數值大於1的時候才做呼叫,因為否則函式會進入死迴圈(an infinite recursive loop),當引數到達0的時候,函式不繼續用負數乘下去(最終可能導致執行時的堆疊溢位錯誤(stack overflow error)。
這個函式有一定的侷限性,為簡單起見,函式設計中使用的資料型別為長整型(long)。在實際的標準系統中,長整型long無法儲存12!以上的階乘值。
函式的宣告(Declaring functions)
到目前為止,我們定義的所有函式都是在它們第一次被呼叫(通常是在main中)之前,而把main 函式放在最後。如果重複以上幾個例子,但把main 函式放在其它被它呼叫的函式之前,你就會遇到編譯錯誤。原因是在呼叫一個函式之前,函式必須已經被定義了,就像我們前面例子中所做的。
但實際上還有一種方法來避免在main 或其它函式之前寫出所有被他們呼叫的函式的程式碼,那就是在使用前先宣告函式的原型定義。宣告函式就是對函式在的完整定義之前做一個短小重要的宣告,以便讓編譯器知道函式的引數和返回值型別。
它的形式是:
type name ( argument_type1, argument_type2, ...);
它與一個函式的頭定義(header definition)一樣,除了:
- 它不包括函式的內容, 也就是它不包括函式後面花括號{}內的所有語句。
- 它以一個分號semicolon sign (;) 結束。
- 在引數列舉中只需要寫出各個引數的資料型別就夠了,至於每個引數的名字可以寫,也可以不寫,但是我們建議寫上。
例如:
// 宣告函式原型 #include <iostream.h> void odd (int a); void even (int a); int main () { int i; do { cout << "Type a number: (0 to exit)"; cin >> i; odd (i); } while (i!=0); return 0; } void odd (int a) { if ((a%2)!=0) cout << "Number is odd.\n"; else even (a); } void even (int a) { if ((a%2)==0) cout << "Number is even.\n"; else odd (a); } |
Type a number (0 to exit): 9 Number is odd. Type a number (0 to exit): 6 Number is even. Type a number (0 to exit): 1030 Number is even. Type a number (0 to exit): 0 Number is even. |
這個例子的確不是很有效率,我相信現在你已經可以只用一半行數的程式碼來完成同樣的功能。但這個例子顯示了函式原型(prototyping functions)是怎樣工作的。並且在這個具體的例子中,兩個函式中至少有一個是必須定義原型的。
這裡我們首先看到的是函式odd 和even的原型:
void odd (int a);void even (int a);
這樣使得這兩個函式可以在它們被完整定義之前就被使用,例如在main中被呼叫,這樣main就可以被放在邏輯上更合理的位置:即程式程式碼的開頭部分。
儘管如此,這個程式需要至少一個函式原型定義的特殊原因是因為在odd 函式裡需要呼叫even 函式,而在even 函式裡也同樣需要呼叫odd函式。如果兩個函式任何一個都沒被提前定義原型的話,就會出現編譯錯誤,因為或者odd 在even 函式中是不可見的(因為它還沒有被定義),或者even 函式在odd函式中是不可見的。
很多程式設計師建議給所有的函式定義原型。這也是我的建議,特別是在有很多函式或函式很長的情況下。把所有函式的原型定義放在一個地方,可以使我們在決定怎樣呼叫這些函式的時候輕鬆一些,同時也有助於生成標頭檔案。