參數按數值傳遞和按地址傳遞(Arguments passed by value and by reference)
到目前爲止,我們看到的所有函數中,傳遞到函數中的參數全部是按數值傳遞的(by value)。也就是說,當我們調用一個帶有參數的函數時,我們傳遞到函數中的是變量的數值而不是變量本身。 例如,假設我們用下面的代碼調用我們的第一個函數addition :
z = addition ( x , y );
在這個例子裏我們調用函數addition 同時將x和y的值傳給它,即分別爲5和3,而不是兩個變量:
這樣,當函數addition被調用時,它的變量a和b的值分別變爲5和3,但在函數addition內對變量a 或b 所做的任何修改不會影響變量他外面的變量x 和 y 的值,因爲變量x和y並沒有把它們自己傳遞給函數,而只是傳遞了他們的數值。
但在某些情況下你可能需要在一個函數內控制一個函數以外的變量。要實現這種操作,我們必須使用按地址傳遞的參數(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 even (int a);
這樣使得這兩個函數可以在它們被完整定義之前就被使用,例如在main中被調用,這樣main就可以被放在邏輯上更合理的位置:即程序代碼的開頭部分。
儘管如此,這個程序需要至少一個函數原型定義的特殊原因是因爲在odd 函數裏需要調用even 函數,而在even 函數裏也同樣需要調用odd函數。如果兩個函數任何一個都沒被提前定義原型的話,就會出現編譯錯誤,因爲或者odd 在even 函數中是不可見的(因爲它還沒有被定義),或者even 函數在odd函數中是不可見的。
很多程序員建議給所有的函數定義原型。這也是我的建議,特別是在有很多函數或函數很長的情況下。把所有函數的原型定義放在一個地方,可以使我們在決定怎樣調用這些函數的時候輕鬆一些,同時也有助於生成頭文件。