owned this note
owned this note
Published
Linked with GitHub
# 自定義函數 function
函數將一些程式碼包成一個個子任務,每次需要時直接呼叫即可。
除了有助整理邏輯,適當地包裝重覆的程式碼,也能有效降低修改時的風險。
例如將計算總和包裝成 calc_sum() 後,求平均的程式碼會變成這樣:
```cpp=
double avg = calc_sum(ary, n) * 1.0 / n;
```
而求兩個陣列平均誰大誰小,則可簡化成:
```cpp=
if (calc_sum(ary0, n0) < calc_sum(ary1, n1))
{
// ...
}
```
相對於寫下求總和的迴圈,呼叫函數可以從命名更容易理解它的用意,
且在其他地方需要計算總和時,可以不用再寫同樣的程式碼。
萬一寫錯,也只需修改函數內的程式碼,無需同時修改每個求平均的地方。
## 宣告 declaration
:::info
宣告函數分成四個部份:函數型別、名字、參數、以及本體。
:::
例如宣告一個函數 `f(x) = 2x - 1`,則分析一下它的四個部份:
- 名字:`f`,有名字才知道是在叫誰
- 參數:一個整數 `x`,決定呼叫時需要提供的資訊
- 型別:整數 `int`,決定函數執行結果的型別
- 本體:計算 `2x - 1` 用的,由大括號包起的一段程式碼
```cpp=
// 結果型別 名字(參數列表)
// {
// 本體
// }
int f(int x)
{
int res = x*2 - 1;
return res;
}
```
以上程式碼宣告一個名為 f 的函數,要求一個整數參數 x,
計算出整數結果 `2x - 1` 並回報。
### 函數的宣告位置
由於 main() 也是個自定義函數,和其它自定義函數應是對等關係,
因此必須宣告在 main() 之外;
又,和變數一樣,函數也必須在使用前宣告,因此通常宣告於 main() 之上。
例如以下是一個輸入 x 輸出 f(x) 的範例:
```cpp=
#include <iostream>
using namespace std;
int f(int x)
{
int res = x*2 - 1;
return res;
}
int main()
{
int x, y;
cin >> x;
y = f(x);
cout << y << '\n';
return 0;
}
```
有興趣可以試試將 f() 的宣告放在 main() 之下,看看會發生什麼事。
### 僅宣稱函數的存在
如果真的非常想將函數宣告於 main() 下方,或者有函數互相呼叫等需求,
可先宣稱「有這樣子的函數存在」但略過本體不寫。
最終還是必須補上本體,且必須與宣告的樣子一致。
```cpp=
#include <iostream>
using namespace std;
int main()
{
int x, y;
// ↓↓↓↓ 先宣稱「有這樣子的函數存在」但略過本體 ↓↓↓↓
int f(int);
// ↑↑↑↑ 先宣稱「有這樣子的函數存在」但略過本體 ↑↑↑↑
cin >> x;
y = f(x);
cout << y << '\n';
return 0;
}
// 宣告在全域的 f() 本體
int f(int x)
{
int res = x*2 - 1;
return res;
}
```
無本體時,宣告在全域或區域皆可,只有本體的宣告必須在全域。
無本體時參數的名字可以忽略,只須寫下型別。
有興趣可以試試宣稱存在的函數與實際本體不符合時,會發生什麼事。
## 函數呼叫 function call
呼叫函數的名字,加上小括號 () 傳遞必要的參數後,
就能執行該函數,並取得函數回報的執行結果。
```cpp=
f(5);
```
函數呼叫本身在 C++ 中,代表一個運算元,
其值為回報的結果,型別為函數型別。
按上述的宣告,`f(5)` 代表著整數 9。
由於是運算元,可以參與運算式。
```cpp=
// 2x10 - 1
int y = f(10);
// 5 * (2x10 - 1) + (2x9 - 1)
int y2 = 5 * f(10) + f(9);
cout << y << ' ' << y2 << '\n';
```
### 執行流程的變化
程式的執行是有順序的,每行程式碼都仰賴於它上一行程式碼執行完畢的結果。
因此,每行程式碼需等待它上一行程式碼執行完畢。
函數呼叫本身代表著函數所回報的計算結果,
因此,當運算式需要知道它所代表的值,才能繼續時,
整個流程就會暫停下來,先執行函數呼叫,等執行完回報結果,才會繼續進行。
```cpp=
// 這行在賦值前必須算出 f(5) 才知道賦值的內容,因此必須等待 f(5) 執行完畢
int x = f(5);
// 這行仰賴上一行執行完的狀況,因此必須等待上一行執行完畢
int y = x * 2;
```
:::warning
運算式不見得需要知道所有的值,才能繼續進行下去。
因此,在運算式不需要知道函數呼叫的值時,函數是不會被呼叫的。
:::
以下是個有被寫進運算式,但因為不需要知道它的值,實際沒被呼叫的例子:
```cpp=
int g(int t)
{
cout << "g(" << t << ")\n";
return t;
}
```
這樣可以透過輸出判斷是否有被呼叫,而 main() 則準備有寫進去、
但實際可能不需要被計算,因此沒被呼叫的程式碼:
```cpp=
int q;
cin >> q;
if (q && g(8))
{
cout << "executed!!\n";
}
```
在輸入為 0 時,由於 && 已經確定不成立,因此不需要知道 g(8) 的值;
輸入非 0 時,則仰賴 g(8) 的值,因此會被呼叫。
## 參數 parameter
宣告時,小括號中可指定任意數量的參數,呼叫時必須依照要求全部提供。
宣告參數時需指定型別和名稱,像一般的變數宣告一樣。
```cpp=
int max(int p, int q)
{
if (p > q)
{
return p;
}
return q;
}
```
上述的程式碼要求兩個整數參數,回傳較大的那一個。
呼叫時傳遞的參數,會被依順序**複製**到對應的變數中。
```cpp=
int t = max(16*9, 18*8);
```
上述程式碼會先將 $16 \times 9$ 的結果複製一份放在變數 p,
將 $18 \times 8$ 的結果複製一份放在變數 q,
再開始執行函數的本體。
:::warning
實際上 C++ 在參數上存在更多的細節,
例如傳值傳址傳參考、重載、參數預設值、可省略參數、可變數量參數…等,
由於大多與競賽並不是那麼相關,在此僅列出一些關鍵字,
有興趣者可自行查閱相關資料。
:::
### 參數的複製
由於參數是複製一份,因此在函數內進行的修改,並不會影響原本的變數。
```cpp=
int twice(int x)
{
x *= 2;
return x;
}
```
看似會將參數變成兩倍,實際上只對 twice() 抄寫的那一份 x 做出變更。
```cpp=
int x = 5;
twice(x);
cout << x << '\n';
```
在執行後,x 的值一樣是 5。
只有 twice 抄的那份會被變更,原本的不會。
也因為如此,我們才能傳遞一些不能被變更的東西作為參數。
試想,如果傳入的東西是有可能會被變更的,那以下程式碼會發生什麼事?
```cpp=
twice(5);
twice(2+3*6);
```
顯然常數 `5` 和運算式 `2+3*6` 都是不能被更動、賦值的東西。
### 當參數型別為陣列時
宣告時同樣加中括號,表示它是個陣列;但不需註明長度。
若有加註長度,會被省略不看。
```cpp=
double calc_avg(int ary[], int n)
{
int sum, i;
for (i=0, sum=0; i<n; i++)
{
sum += ary[i];
}
return (double)sum / n;
}
```
以上是其中一個參數為陣列的例子。
```cpp=
int ary[16], n, i;
cin >> n;
for (i=0; i<n; i++)
{
cin >> ary[i];
}
double avg = calc_avg(ary, n);
cout << avg << '\n';
```
:::info
值得注意的是:參數為陣列時,是把陣列所存放的位置,抄寫一份傳遞進去,
因此修改陣列裡存放的內容時,是會修改到原本陣列的。
:::
:::warning
或許有些範例會以 `int *ary` 的方式來宣告參數,
可達成的效果基本上是相同的,只是在意義上容易被解讀成其它意思。
:::
### 當參數型別為多維陣列時
由於多維陣列在位址計算上,依賴於第一維以外每一維的大小,
因此除了必須加註,也必須保持一致才行。
```cpp=
int get_sum(int (*matrix)[4], int n)
{
int i, j, sum;
for (i=0; i<n; i++)
{
for (j=0; j<4; j++)
{
sum += matrix[i][j];
}
}
return sum;
}
```
使用 `int (*matrix)[4]` 宣告一個陣列,
其每一個 element 是一個「長度 4 的一維 int 陣列」。
```cpp=
int a[8][4], b[4][8];
// ok
cout << get_sum(a, 8) << '\n';
// compile error
cout << get_sum(b, 4) << '\n';
```
這是因為 b 的每個 element 是一個「長度 8 的一維 int 陣列」,
並不是「長度 4 的一維 int 陣列」,型別不符。
就像要求 string 型別的參數,呼叫時卻傳入 int 是類似的感覺。
明明要的是食物,卻被塞一些不能吃的東西。
## 回傳值 return value
函數的本體可透過 `return` 來回傳一個值作為計算結果,
被回傳的值其型別必須與函數型別一致。
一旦執行到 `return` 函數便會停止執行。
```cpp=
int divide(int p, int q)
{
if (q == 0)
{
cout << "NOOOOO you CANNOT divide 0!!\n";
return 0;
cout << "after return 0, this won't be executed.\n";
}
return p / q;
}
```
例如上述程式碼,在 q == 0 時便會因 `return 0;` 而結束執行,
因此不會執行到 `p / q` 這個會導致除以 0 的地方。
如果不夠明顯,可以看第 7 行的 cout 是否有被執行。
## void 型別的函數
如果這函數僅用來執行某些程式碼,不須回報任何結果,
可將型別宣告為 void。
它將不再能回傳任何值,也不能作為運算元參與計算。
仍然可以使用 return 結束函數執行,但不能回傳任何值。
```cpp=
void output(int t)
{
if (t < 0)
{
cout << "negative.\n";
// return 仍會結束執行,但不得傳回任何值,必須接著分號 ;
return;
}
cout << "non-negative.\n";
}
```
上述程式碼宣告一個型別為 void 的函數,
將只能執行,不會回報任何值,無法參與計算。
```cpp=
// compile error
int a = output(-4);
// 僅能呼叫不能參與計算
output(-4);
```
## 歡樂練習時間?
由於難以找到非使用函數不可的題目,
且使用函數較不使用函數明顯有利的情況,多半是程式足夠大而繁複的時候,
因此沒有練習題。
若想練習,可以試試完成以下幾個函數並自行撰寫 main() 測試:
```cpp=
int get_abs(int t)
{
// 回傳 t 的絕對值
}
```
```cpp=
int is_prime(int n)
{
// 回傳 0 代表 n 不是質數;回傳 0 以外的整數代表 n 是質數
}
```
```cpp=
int get_gcd(int p, int q)
{
// 回傳 p, q 的最大公因數
}
```
```cpp=
int get_max(int ary[], int st, int ed)
{
// 回傳陣列 ary 中,範圍 [st, ed] 間最大的元素
}
```
```cpp=
int max3(int p, int q, int r)
{
// 回傳 p, q, r 中最大的整數
}
```
```cpp=
int str_cmp(string p, string q)
{
// 比較字串 p, q 的字典序,回傳負數代表 p 字典序比 q 小;
// 回傳正數代表 p 字典序比 q 大;其它情況一律回傳 0
}
```
```cpp=
char case_change(char c)
{
// 如果 c 是大寫,回傳它的小寫;
// 如果 c 是小寫,回傳它的大寫;
// 如果都不是,回傳 c 本身
}
```
```cpp=
const int DIGIT = 0;
const int UPPER = 1;
const int LOWER = 2;
const int OTHER = 3;
string pickup(string s, int pick_type, bool trans)
{
// 從 s 取出想要的文字,按照原順序組成字串傳回
// pick_type 只會有上面定義的四種可能
// trans 表示在 UPPER 或 LOWER 時,不符合的字母要不要變換後加入
// 例如 UPPER 時,trans 如果是 true 表示小寫字母也要取,且轉成大寫
// trans 不影響字母以外的文字
}
// 以下是呼叫例
string t = "3A7k5a g6I4!";
cout << pickup(t, DIGIT, false) << '\n'; // "37564"
cout << pickup(t, OTHER, true) << '\n'; // " !"
cout << pickup(t, LOWER, false) << '\n'; // "kag"
cout << pickup(t, UPPER, true) << '\n'; // "AKAGI"
```
{%hackmd @sa072686/__style %}
###### tags: `競程:初章`, `競程`