---
# System prepended metadata

title: 第六章 函式 筆記

---

# 第六章 函式 筆記

## 函式基礎

- 函式定義：由一個回傳型別（return type）、一個名稱和一串由 0 或多個參數（parameter）的列表以及函式主體所構成
- 呼叫運算子（call operator）：呼叫運算子是一對圓括號 ()，作用於一個運算式，該運算式是函式或者指向函式的指標，圓括號內是逗號分隔開的引數（argument）
- 函式的呼叫過程：
  - 呼叫端函式（calling function）的執行被中斷
  - 被呼叫函式（called function）開始被執行
- 回傳型別：void 表示函式不回傳任何值，且函式的回傳型別不能是陣列型別或函式型別，但可以是指向陣列或函式的指標

參數（parameter）與引數（argument）：
- 無法保證引數被估算（evaluate）的順序，這部分由編譯器實作決定
- 函式呼叫時參數與引數的型別必須相符
- 參數的名稱是選擇性的（optional），所以可以只給型別名稱即可，但無名參數是無法被使用的，因此通常會有名稱
- 即使參數名稱是無名的，一個呼叫仍必須為每個參數提供一個引數，並非無名就可以省略引數的數量

### 區域物件

- 生命周期（lifetimes）：物件的生命週期是程式執行過程中該物件保證存在的一段時間
- 範疇（scope）：一個名稱的範疇是指能夠看到該名稱的範圍
- 區域變數（local variable）：參數和函式主體內部定義的變數統稱為局部變數，它們對於函式而言是局部的，對函是外部而言是隱藏的
- 自動物件（automatic objects）：只存在於區塊執行期間的物件，當該區塊執行結束後，它的值就變成未定義的了
- 區域 static 物件：區域 static 物件的生命週期是從第一次被執行直到程式結束為止

### 函式宣告

- 函式宣告：函式宣告和定義唯一的區別是函式宣告不需要函式主體，改為用一個分號替代，函式宣告主要用於描述該函式的接口，也被稱作函式原型（function prototype）
- 建議在標頭進行函式宣告，變數則在原碼檔（source files）中定義，如果放在標頭中隔離，若介面有變，僅需改變一個檔案即可，但若寫在原碼檔中可能會導致修改不易

### 個別編譯（separate compilation）
使用 g++ a.cc b.cc 會經過前置處理（preprocess）、編譯（compilation）、連結（linker）以生成可執行檔，但使用 g++ -c a.cc b.cc 則只會產生目的碼（object code），副檔名為 .obj（windows）或 .o（linux），接著再使用 g++ a.o b.o，將只呼叫連結器以產生可執行檔

## 引數傳遞

- 參數初始化的型別轉型規則與變數定義的原理相同
- 傳參考（passed by reference）：指參數是參考型別，參考參數是該參考所對應的引數的別名
- 傳值（passed by value）：指參數的值是引數的拷貝，彼此是互相獨立的

### 藉由值傳遞引數

- 當初始化一個非參考型別的參數時，初始值會由拷貝的方式，從引數拷貝到參數
- 函式對參考做的所有操作都不會影響引數
- 指標參數：常用在 C 中，C++ 建議一律使用參考參數取代指標參數

### 藉由參考傳遞引數

- 通過使用參考參數，允許函式該參數所對應的引數的值
- 參考參數直接繫結（bind）到引數物件，而非拷貝
- 使用參考參數可以用於回傳額外的資訊
- 經常使用參考參數可避免過大物件的複製
- void swap(int &v1, int &v2)
- 如果不需要改變參數參考的值，一定要記得加上 const

### const 參數與引數

- 參考的的頂層 const 會被忽略，void func(const int i); 被呼叫時既可以傳入 const int 也可以傳入 int
- 我們可以使用非常數初始化一個底層 const 的物件，但是反過來不行
- 盡量使用常數參考參數

### 陣列參數

- 我們無法拷貝一個陣列，因此不可能以傳值的方式傳遞陣列
- 當我們為函式傳遞一個陣列時，實際上傳遞的是指向陣列第一個元素的地址
- 要注意陣列的實際長度，若越界會導致未定義行為

### main：處理命令列選項

- int main(int argc, char *argv[]){...}
- argc 代表參數的個數（argument count），argv 則代表參數為 c-style 字串的開頭地址的所組成的陣列的第一個元素（為一個 char*）的地址（arguement vector）

### 帶有不定參數的函式

C++11 標準庫提供了兩種主要方式來撰寫函式接受數目不定的引數，如果所有的引數型別都相同，可以傳入名為 initializer_list 的一個程式庫型別，如果引數的型別不同，則我們可以撰寫 variadic template（參數可辨的模板），這將在第 16 章涵蓋，以下僅介紹 initializer_list

C++ 還有提供 ellipsis（省略符號）來傳入數目不定的引數，值得注意的是：這項機能通常只應用於需要與 C 函式界接的程式中，並不常使用

initializer_list 上的運算（C++11）：

| 操作 | 解釋 |
|-----|-----|
| initializer_list\<T> lst; | 默認初始化；型別為 T 的元素所成的一個空串列 |
| initializer_list\<T> lst{a,b,c...}; | lst 有跟初始器（initializer）數目一樣多的元素；這些元素是對應的初始器的拷貝。串列中的元素是 const |
| lst2(lst) | 拷貝或指定一個 initializer_list 並不會拷貝串列中的元素。拷貝之後，原來的跟拷貝的會共用那些元素 |
| lst2 = lst | 同上 |
| lst.size() | 串列中的元素數目 |
| lst.begin() | 回傳指標指向 lst 中第一個元素的地方 |
| lst.end() | 回傳指標指向 lst 中超過最後一個元素一個位置的地方 |

initializer_list 使用範例：

```cpp
void err_msg(ErrCode e, initializer_list<string> il){
    cout << e.msg << endl;
    for (auto bed = il.begin(); beg != il.end(); ++ beg)
        cout << *beg << " ";
    cout << endl;
}

err_msg(ErrCode(0), {"functionX", "okay"});
```

## 回傳型別與 return 述句

### 沒有回傳值的函式

沒有回傳值的 return 述句只能用在回傳型別是 void 的函式中，且回傳 void 的函式並不要求一定要有 return 述句

### 回傳一個值的函式

- return 述句的回傳值的型別必須和函式的回傳型別相同，或者能夠隱含轉換成函式的回傳型別
- 回傳的值用於初始化呼叫調用點的    值的返回：返回的值用于初始化调用点的一个临时量，该临时量就是函式调用的结果。
- 永遠都不要回傳對區域物件的參考或指標
- 回傳的參考是 lvalue：一個函式呼叫是否為 lvalue 取決於該函式的回傳型別，回傳參考的函式呼叫是 lvalue，剩下其他的回傳型別則是 rvalue，假設說 int test(); 這個函式回傳的即是一個 rvalue int
- 串列初始化回傳值：函式可以回傳花括號包圍的值的列表（C++11）
- 主函式 main 的回傳值：如果結尾沒有 return，編譯器將隱含地插入一條回傳 0 的 return 述句，回傳 0 代表執行成功，反之非回傳 0 的則代表執行失敗

### 回傳對陣列的一個指標

- Type (*function (parameter_list))[dimension]
- 可使用型別別名（type alias）簡化： typedef int arrT[10]; 或者 using arrT = int[10]; 然後 arrT* func() {...}
- 使用 decltype：decltype(odd) *arrPtr(int i) {...}
- 使用尾端回傳型別（Trailing Return Type）：在參數列表後面以一個 -> 接著回傳型別：auto func(int i) -> int(*)[10]（C++11）

## 重載的函式

出現在相同範疇內但參數不同的函式就是重載（overload）函式
- main 函式不能重載
- 函式重載和 const 參數：
  - 一個有頂層 const 的參數和沒有頂層 const 的函式是無法區分的：Record lookup(Phone* const)和 Record lookup(Phone*) 無法區分
  - 相反，是否有某個底層 const 參數則可以區分 Record lookup(Account*) 和 Record lookup(const Account*) 可以區分
- 呼叫一個重載函式：呼叫時，將會依據某些步驟進行函式匹配（function matching，又稱作函式解析 function resolution），會導致以下三種結果：
  - 找到最佳匹配（best matching），然後產生程式碼來呼叫那個函式
  -  沒有函式的參數符合呼叫中的引數，編譯器會發出錯誤訊息，指出沒有匹配（no match）
  - 有一個以上的函式符合，而且其中沒有任何一個是最明顯的匹配，這時候編譯器會產生錯誤，指出歧異的呼叫（ambiguous call）
- 重載與範疇：如果在內層範疇宣告了一個名稱，那個名稱就會隱藏在外層的某個範疇中宣告的名稱之使用，且在不同範疇下無法重載函式名稱

## 特殊用途的功能

### 預設引數

- 範例：string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
- 一旦某個參數有了預設引數，那麼它之後的參數都必須要有默認值

### 行內與 constexpr 函式

- 普通函式的缺點：呼叫函式會有額外的工作，因此速度上較慢
- inline 函式可以避免函式呼叫的開銷，請求編譯器在編譯時內部展開，但因為只是一種請求，編譯器可以選擇忽略
- inline 函式應該放在標頭中定義

### constexpr 函式

- 指能被用在常數運算式（constant expression）中使用的一種函式
- constexpr 函式是隱含 inline 的
- 一個 constexpr 函式不一定要回傳一個常數運算式
- constexpr 函式應該放在標頭中定義

### 用於除錯的輔助功能

- assert 前置處理器巨集（preprocessor macro）：assert(expr); 使用前須 #include \<cassert>

- 開關 DEBUG 狀態：assert 的行為取決於名為 NDEBUG 的狀態，如果 NDEBUG 有定義，assert 就什麼都不會做。預設情況下 NDEBUG 並沒有定義，所以 assert 預設就會進行執行時期的檢查

- 使用 -D DEBUG 可以定義 NDEBUG 這個前置處理器變數： g++ -D NDEBUG main.c

```cpp
void print(){
    #ifndef NDEBUG
        cerr << __func__ << "..." << endl;
    #endif
}
```
- 由編譯器預設定義的巨集處理變數：

| 巨集名稱 | 意義 |
|---|---|
| \_\_FILE\_\_ | 含有檔案名稱的字串字面值 |
| \_\_LINE\_\_ | 含有目前行號的整數字面值 |
| \_\_TIME\_\_ | 含有檔案編譯時間的字串字面值 |
| \_\_DATE\_\_ | 含有檔案編譯日期的字串字面值 |

## 函式匹配

### 判斷候選函式（candidate functions）與合用的函式（viable functions）

尋找合適的函式匹配需要以下三步驟：
- 選定候選函式：只要名稱與被呼叫的函式相同，而且在該範疇下可見的名稱即候選函式（candidate function）
- 選擇合用的函式：從候選函式集合中挑選參數數目相同，且每個參數型別都必須匹配或能夠轉換為對應參數的型別即為合用的函式（viable functions）
- 尋找最佳匹配：引數的型別與參數的型別愈相近愈好、匹配優先度愈高，以下為最佳匹配的優先序

## 引數型別之轉換

為了判斷最佳匹配，編譯器會為可用來將每個引數之型別轉為其對應參數之型別的轉換方法做排名：

1. 完全符合，會在下列情況下發生：
  - 引數和參數的型別完全相同
  - 引數是從陣列或函式型別轉為對應的指標型別
  - 頂層的 const 會被加到引數或從之移除
2. 透過 const 轉換的匹配
3. 透過提升（promotion）的匹配
4. 透過算術或指標轉換的匹配
5. 透過類別型別轉換的匹配

## 指向函式的指標

- 函式指標（function pointer）：指向函式的指標
- bool (*pf)(const string &, const string &); pf 函式指標的兩端小括號不可少
- 函式指標能夠直接透過呼叫運算子呼叫函式，不需要先 dereference
- 函式的參數列中使用函式定義會被自動轉為對函式的指標
- 可使用型別別名、decltype、尾端回傳（trailing return）簡化，以方便宣告會回傳函數指標或以函式指標為參數的函式
