函式

函式允許將程式組織成程式碼段,以執行單獨的任務。

在C++中,函式是一組被命名並可以從程式某處呼叫的語句。定義函式的常用語法是:

type name ( parameter1, parameter2, ...) { statements }

其中
- type 是函式返回值的型別。
- name 是函式的可呼叫識別符號。
- parameters(根據需要設定數量):每個引數由一個型別後跟一個識別符號組成,引數之間用逗號分隔。每個引數看起來都非常像一個常規變數宣告(例如:int x),實際上在函式內充當一個區域性於該函式的常規變數。引數的目的是允許從呼叫它的位置向函式傳遞實參。
- statements 是函式的體。它是一個用大括號 { } 包圍的語句塊,指定函式實際執行的操作。

讓我們來看一個例子

// function example
#include <iostream>
using namespace std;

int addition (int a, int b)
{
  int r;
  r=a+b;
  return r;
}

int main ()
{
  int z;
  z = addition (5,3);
  cout << "The result is " << z;
}
The result is 8

此程式分為兩個函式:additionmain。請記住,無論它們的定義順序如何,C++程式總是從呼叫 main 開始。實際上,main 是唯一自動呼叫的函式,任何其他函式的程式碼只有在其函式被直接或間接從 main 呼叫時才會執行。

在上面的例子中,main 以宣告型別為 int 的變數 z 開始,緊接著執行第一個函式呼叫:它呼叫 addition。函式呼叫遵循與函式宣告非常相似的結構。在上面的例子中,addition 的呼叫可以與其幾行之前的定義進行比較。


函式宣告中的引數與函式呼叫中傳遞的實參有明確的對應關係。呼叫將兩個值 53 傳遞給函式;這些對應於為函式 addition 宣告的引數 ab

當函式在 main 中被呼叫時,控制權被傳遞給函式 addition:這裡,main 的執行將暫停,直到 addition 函式結束才會恢復。在函式呼叫時,兩個實參(53)的值被複制到函式內的區域性變數 int aint b 中。

然後,在 addition 內部,聲明瞭另一個區域性變數(int r),並透過表示式 r=a+b,將 ab 的結果賦給 r;在本例中,當 a 為 5 且 b 為 3 時,將 8 賦給了 r

函式中的最後一條語句

1
return r;

結束函式 addition,並將控制權返回到函式被呼叫的位置;在本例中是返回到函式 main。在這一精確時刻,程式在 main 中恢復其程序,精確地回到被 addition 呼叫中斷的那個點。但此外,由於 addition 有一個返回型別,因此呼叫被評估為一個具有值,該值是結束 addition 的 return 語句中指定的值:在這種特定情況下,是區域性變數 r 的值,在 return 語句時該值為 8。


因此,對 addition 的呼叫是一個具有函式返回值值的表示式,在這種情況下,該值 8 被賦給 z。這就像整個函式呼叫(addition(5,3))被它返回的值(即 8)所替換一樣。

然後 main 簡單地透過呼叫列印此值

1
cout << "The result is " << z;

一個函式實際上可以在程式中被多次呼叫,並且其引數自然不限於字面量。

// function example
#include <iostream>
using namespace std;

int subtraction (int a, int b)
{
  int r;
  r=a-b;
  return r;
}

int main ()
{
  int x=5, y=3, z;
  z = subtraction (7,2);
  cout << "The first result is " << z << '\n';
  cout << "The second result is " << subtraction (7,2) << '\n';
  cout << "The third result is " << subtraction (x,y) << '\n';
  z= 4 + subtraction (x,y);
  cout << "The fourth result is " << z << '\n';
}
The first result is 5
The second result is 5
The third result is 2
The fourth result is 6

與前一個示例中的 addition 函式類似,此示例定義了一個 subtract 函式,該函式僅返回其兩個引數的差。這次,main 多次呼叫此函式,演示了更多可能的函式呼叫方式。

讓我們檢查每一次呼叫,同時記住每次函式呼叫本身就是一個被評估為其返回值的表示式。同樣,您可以將其視為函式呼叫被返回的值所替換。

1
2
z = subtraction (7,2);
cout << "The first result is " << z;

如果我們用它返回的值(即 5)替換函式呼叫,我們會得到

1
2
z = 5;
cout << "The first result is " << z;

透過相同的過程,我們可以解釋
1
cout << "The second result is " << subtraction (7,2);


1
cout << "The second result is " << 5;

因為 5 是 subtraction (7,2) 返回的值。

在...的情況下

1
cout << "The third result is " << subtraction (x,y);

傳遞給 subtraction 的實參是變數而不是字面量。這也是有效的,並且工作正常。函式呼叫時,函式使用 xy 的值,分別作為 5 和 3,返回 2 作為結果。

第四次呼叫又類似了

1
z = 4 + subtraction (x,y);

唯一增加的是,現在函式呼叫也是加法運算的運算元。同樣,結果與函式呼叫被其結果替換時相同:6。請注意,由於加法的交換律,上面也可以寫成

1
z = subtraction (x,y) + 4;

結果完全相同。還請注意,分號不一定出現在函式呼叫之後,而是像往常一樣,出現在整個語句的末尾。同樣,可以透過用其返回的值替換函式呼叫來輕鬆看出其背後的邏輯。

1
2
z = 4 + 2;    // same as z = 4 + subtraction (x,y);
z = 2 + 4;    // same as z = subtraction (x,y) + 4; 

沒有返回型別的函式。void 的使用

上面展示的函式語法

type name ( argument1, argument2 ...) { statements }

要求宣告以型別開頭。這是函式返回值的型別。但是如果函式不需要返回值怎麼辦?在這種情況下,要使用的型別是 void,它是一個特殊的型別,表示沒有值。例如,一個僅僅列印訊息的函式可能不需要返回值。

// void function example
#include <iostream>
using namespace std;

void printmessage ()
{
  cout << "I'm a function!";
}

int main ()
{
  printmessage ();
}
I'm a function!

void 也可以用於函式引數列表,以明確指定函式在被呼叫時實際上不接受任何引數。例如,printmessage 可以宣告為

1
2
3
4
void printmessage (void)
{
  cout << "I'm a function!";
}

在 C++ 中,可以使用空的引數列表代替 void,其含義相同,但 void 在引數列表中的使用是由 C 語言普及的,在那裡這是必需的。

在任何情況下都不可選的是函式名後面的括號,無論是在宣告還是在呼叫它時。即使函式不接受引數,也必須始終在函式名後附加至少一對空括號。看看之前例子中 printmessage 是如何被呼叫的。

1
printmessage ();

括號是將函式與其他宣告或語句區分開來的東西。以下不會呼叫函式

1
printmessage;

main 的返回值

您可能已經注意到 main 的返回型別是 int,但本章及之前章節中的大多數示例實際上都沒有從 main 返回任何值。

好了,這裡有一個訣竅:如果 main 的執行正常結束而沒有遇到 return 語句,編譯器會假定函式以隱式 return 語句結束。

1
return 0;

請注意,出於歷史原因,這僅適用於函式 main。所有其他具有返回型別的函式都應以正確的 return 語句結束,該語句包含一個返回值,即使該返回值從未使用過。

main 返回零(隱式或顯式)時,環境將其解釋為程式成功結束。main 可以返回其他值,並且某些環境以某種方式允許呼叫者訪問該值,儘管這種行為不是必需的,也並非在不同平臺之間可移植。main 的值保證在所有平臺上以相同方式解釋為:

描述
0程式成功執行
EXIT_SUCCESS程式成功執行(與上面相同)。
此值在標頭檔案 <cstdlib> 中定義。
EXIT_FAILURE程式失敗。
此值在標頭檔案 <cstdlib> 中定義。

由於 main 的隱式 return 0; 語句是一個棘手的例外,一些作者認為顯式編寫該語句是一種好習慣。

按值和按引用傳遞引數

在前面的函式中,引數始終是按值傳遞的。這意味著,在呼叫函式時,傳遞給函式的是呼叫時這些引數的值,這些值被複制到函式引數所表示的變數中。例如,考慮

1
2
int x=5, y=3, z;
z = addition ( x, y );

在這種情況下,函式 addition 傳遞了 5 和 3,它們分別是 xy 值的副本。這些值(5 和 3)用於初始化函式定義中設定的引數變數,但這些變數在函式內的任何修改都不會影響函式外部的變數 xy 的值,因為在呼叫時 xy 本身並未傳遞給函式,只傳遞了它們當時值的副本。


然而,在某些情況下,從函式內部訪問外部變數可能會很有用。為此,可以按引用而不是按值傳遞引數。例如,此程式碼中的 duplicate 函式會複製其三個引數的值,從而導致用作引數的變數實際上被呼叫修改。

// passing parameters by reference
#include <iostream>
using namespace std;

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

為了訪問其引數,函式將其引數宣告為引用。在 C++ 中,引用透過引數型別後的 ampersand (&) 來指示,就像上面示例中 duplicate 所接受的引數一樣。

當一個變數按引用傳遞時,傳遞的不再是副本,而是變數本身,函式引數所標識的變數與傳遞給函式的引數 somehow 關聯起來,並且函式內對其相應區域性變數的任何修改都會反映在呼叫中作為引數傳遞的變數上。



實際上,abc 成為函式呼叫(xyz)的實參的別名,並且 a 在函式內的任何更改實際上都會修改函式外的變數 x。任何對 b 的更改都會修改 y,對 c 的更改都會修改 z。這就是為什麼在示例中,當函式 duplicate 修改變數 abc 的值時,xyz 的值會受到影響。

如果不是將 duplicate 定義為

1
void duplicate (int& a, int& b, int& c)

而是將其定義為沒有 ampersand 符號,如

1
void duplicate (int a, int b, int c)

變數將不會按引用傳遞,而是按值傳遞,而是建立其值的副本。在這種情況下,程式的輸出將是 xyz 的值,但未修改(即 1、3 和 7)。

效率考量和 const 引用

按值傳遞引數呼叫函式會複製這些值。對於 int 等基本型別,這是一個相對便宜的操作,但如果引數是大型複合型別,可能會導致一定的開銷。例如,考慮以下函式

1
2
3
4
string concatenate (string a, string b)
{
  return a+b;
}

此函式將兩個字串作為引數(按值)傳遞,並返回連線它們的連線結果。透過按值傳遞實參,函式強制 ab 成為呼叫函式時傳遞給函式的實參的副本。如果這些是長字串,這可能意味著僅為了函式呼叫就需要複製大量資料。

但是,如果兩個引數都按引用傳遞,則可以完全避免此複製。

1
2
3
4
string concatenate (string& a, string& b)
{
  return a+b;
}

按引用傳遞的引數不需要複製。函式直接操作(別名)傳遞的字串,最多可能意味著將某些指標傳遞給函式。在這方面,採用引用的 concatenate 版本比採用值的版本更有效,因為它不需要複製昂貴的字串。

另一方面,帶有引用引數的函式通常被認為是修改傳遞實參的函式,因為這就是引用引數的實際用途。

解決方案是讓函式保證其引用引數不會被該函式修改。這可以透過將引數限定為常量來完成。

1
2
3
4
string concatenate (const string& a, const string& b)
{
  return a+b;
}

透過將它們限定為 const,函式被禁止修改 ab 的值,但可以作為引用(實參的別名)訪問它們的值,而無需實際複製字串。

因此,const 引用提供了與按值傳遞引數類似的功能,但對於大型型別引數,效率更高。這就是為什麼它們在 C++ 中對複合型別的引數非常受歡迎的原因。但請注意,對於大多數基本型別,效率上沒有明顯的差異,在某些情況下,const 引用可能效率更低!

行內函數

呼叫函式通常會產生一定的開銷(堆疊引數、跳轉等),因此對於非常短的函式,將函式的程式碼插入到呼叫它的地方可能比執行正式呼叫函式的過程更有效。

在函式宣告前加上 inline 說明符會通知編譯器,對於特定函式,首選內聯展開而不是常規函式呼叫機制。這根本不會改變函式行為,只是用於建議編譯器將函式體生成的程式碼插入到函式被呼叫的每個點,而不是透過常規函式呼叫來呼叫。

例如,上面的 concatenate 函式可以宣告為內聯,如下所示:

1
2
3
4
inline string concatenate (const string& a, const string& b)
{
  return a+b;
}

這會通知編譯器,在呼叫 concatenate 時,程式更傾向於內聯展開該函式,而不是執行常規呼叫。inline 只在函式宣告中指定,在呼叫時不需要。

請注意,大多數編譯器即使沒有顯式標記 inline 說明符,也會最佳化程式碼以生成行內函數。因此,此說明符僅表示編譯器首選此函式的內聯,儘管編譯器可以自由地不內聯它,並進行其他最佳化。在 C++ 中,最佳化是編譯器負責的任務,只要生成的結果行為與程式碼指定的行為一致,編譯器就可以自由生成任何程式碼。

引數的預設值

在 C++ 中,函式也可以有可選引數,這些引數在呼叫時不需要實參,例如,一個有三個引數的函式可以只用兩個引數來呼叫。為此,函式必須為其最後一個引數包含一個預設值,該預設值在呼叫引數較少的函式時會被函式使用。例如:

// default values in functions
#include <iostream>
using namespace std;

int divide (int a, int b=2)
{
  int r;
  r=a/b;
  return (r);
}

int main ()
{
  cout << divide (12) << '\n';
  cout << divide (20,4) << '\n';
  return 0;
}
6
5

在此示例中,有兩個對函式 divide 的呼叫。在第一個呼叫中:

1
divide (12)

呼叫只向函式傳遞了一個引數,儘管函式有兩個引數。在這種情況下,函式假定第二個引數為 2(注意函式定義,它宣告其第二個引數為 int b=2)。因此,結果是 6。

在第二次呼叫中:

1
divide (20,4)

呼叫向函式傳遞了兩個引數。因此,將忽略 b 的預設值(int b=2),b 將採用作為實參傳遞的值,即 4,從而產生結果 5。

宣告函式

在 C++ 中,識別符號只能在聲明後才能在表示式中使用。例如,變數 x 在宣告之前不能被使用,例如:

1
int x;

函式也是如此。函式不能在宣告之前被呼叫。這就是為什麼在前面所有的函式示例中,函式總是定義在 main 函式之前,而 main 函式是從那裡呼叫其他函式的。如果 main 定義在其他函式之前,這將違反函式必須在使用前宣告的規則,因此將無法編譯。

函式原型可以在不完全定義函式的情況下宣告,只需提供足夠的資訊即可瞭解函式呼叫中涉及的型別。當然,函式必須在其他地方定義,比如程式碼的後面。但至少,一旦這樣宣告,就可以呼叫它了。

宣告應包含所有涉及的型別(返回型別及其引數的型別),使用與函式定義中相同的語法,但將函式體(語句塊)替換為一個結束分號。

引數列表不需要包含引數名稱,只需要它們的型別。儘管如此,仍然可以指定引數名稱,但它們是可選的,並且不一定需要與函式定義中的名稱匹配。例如,一個名為 protofunction 且有兩個 int 引數的函式可以用以下任一語句宣告:

1
2
int protofunction (int first, int second);
int protofunction (int, int);

無論如何,為每個引數包含名稱總能提高宣告的可讀性。

// declaring functions prototypes
#include <iostream>
using namespace std;

void odd (int x);
void even (int x);

int main()
{
  int i;
  do {
    cout << "Please, enter number (0 to exit): ";
    cin >> i;
    odd (i);
  } while (i!=0);
  return 0;
}

void odd (int x)
{
  if ((x%2)!=0) cout << "It is odd.\n";
  else even (x);
}

void even (int x)
{
  if ((x%2)==0) cout << "It is even.\n";
  else odd (x);
}
Please, enter number (0 to exit): 9
It is odd.
Please, enter number (0 to exit): 6
It is even.
Please, enter number (0 to exit): 1030
It is even.
Please, enter number (0 to exit): 0
It is even.

這個例子確實不是關於效率的。你可能可以自己寫一個程式碼行數少一半的該程式的版本。無論如何,這個例子說明了函式是如何在定義之前宣告的。

以下行

1
2
void odd (int a);
void even (int a);

聲明瞭函式的原型。它們已經包含了呼叫它們所需的所有資訊:它們的名稱、引數型別以及返回型別(在本例中為 void)。透過這些原型宣告,可以在它們完全定義之前呼叫它們,例如,允許將呼叫它們的函式(main)放在這些函式實際定義之前。

但是,在定義之前宣告函式不僅對於重新組織程式碼中的函式順序很有用。在某些情況下,例如在本例中,至少有一個宣告是必需的,因為 oddeven 是相互呼叫的;在 odd 中有一個對 even 的呼叫,在 even 中有一個對 odd 的呼叫。因此,沒有辦法組織程式碼使得 oddeven 之前定義,並且 evenodd 之前定義。

遞迴

遞迴是函式能夠自我呼叫的屬性。它對於某些任務很有用,例如排序元素或計算數字的階乘。例如,為了獲得一個數字的階乘(n!),數學公式是:

n! = n * (n-1) * (n-2) * (n-3) ... * 1
更具體地說,5!(5 的階乘)將是:

5! = 5 * 4 * 3 * 2 * 1 = 120
一個遞迴計算 C++ 中這個值的函式可以是:

// factorial calculator
#include <iostream>
using namespace std;

long factorial (long a)
{
  if (a > 1)
   return (a * factorial (a-1));
  else
   return 1;
}

int main ()
{
  long number = 9;
  cout << number << "! = " << factorial (number);
  return 0;
}
9! = 362880

注意,在 factorial 函式中,我包含了一個對自身的呼叫,但這僅在傳遞的引數大於 1 時才發生,否則,函式將執行無限遞迴迴圈,一旦到達 0,它將繼續乘以所有負數(可能在執行時某個時刻導致堆疊溢位)。
Index
目錄