天天看點

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,而不是兩個變量:

C++ 控制結構和函數(三)—— 函數II(Functions II)

這樣,當函數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)一個變量的時候,我們是在傳遞這個變量本身,我們在函數中對變量所做的任何修改将會影響到函數外面被傳遞的變量。

C++ 控制結構和函數(三)—— 函數II(Functions II)

用另一種方式來說,我們已經把變量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。

C++ 控制結構和函數(三)—— 函數II(Functions II)

這種用位址符 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函數中是不可見的。

很多程式員建議給所有的函數定義原型。這也是我的建議,特别是在有很多函數或函數很長的情況下。把所有函數的原型定義放在一個地方,可以使我們在決定怎樣調用這些函數的時候輕松一些,同時也有助于生成頭檔案。

繼續閱讀