← 09/24|流程控制與函式基礎(Ch 2、Ch 3) | 回總覽 | 10/08|陣列(Ch 5) →
對應課本習題:Ch4: 3, 7, 8, 9, 14, 17
這幾題各要用到什麼(動手前先看)
主課的上機考幾乎就是這些題目,所以每一題都自己寫過。下表是每題需要的東西(我的歸納,不是題目本文)與本系列對應的練習:
| 課本題號 | 要用到的東西 | 先練 |
|---|---|---|
| Ch4-3 | 選單購買:用參考參數在函式裡更新餘額,迴圈到買不起為止 | Q5、Q7、Q10 |
| Ch4-7/8/9 | 單位換算:輸入、計算、輸出各一個函式,if-else 決定換算方向,重複到使用者結束 |
Q10 |
| Ch4-14 | 兩個時刻相差幾分鐘(要處理跨午夜)——/、% 拆時分、函式回傳 int |
09/17 Q6、Q2 |
| Ch4-17 | 從分數檔找某個人的分數、跟全部平均比——09/24 的〈從檔案讀入〉+函式回傳值 | 09/24 讀檔範例、Q2 |
這週要會什麼
1 | 傳值 vs 傳參考 → const 參考 → 函式重載 → 預設引數 → assert/stub/driver |
這週要記的東西不多,但筆試超愛考:給你一段程式,問呼叫函式之後外面的變數變成多少。
傳值(call-by-value):函式改不到外面
1 |
|
白話說:呼叫 addOne(a) 時,電腦把 a 的值影印一份給函式。函式在影本上塗改,正本完全沒事。
傳參考(call-by-reference):函式可以改到外面
在參數型別後面加一個 &,意思是「這個參數就是外面那個變數本人,不是複製品」。(注意這個 & 跟 09/17 的 &&「且」沒有關係;它還有第三個用法「取記憶體位址」,11/19 講指標才會遇到。)
1 |
|
白話說:int& x 不是複製,而是替 a 取了一個小名。在函式裡叫 x,改的就是外面的 a 本人。兩種傳法擺在一起比較:
1 | 傳值 main: a=5 ──複製──▶ addOne: x=5 → x=6 (函式結束 x 消失,a 還是 5) |
經典應用——交換兩個變數:
1 | void swapValues(int& a, int& b) { |
如果參數少了 &,這個函式就完全沒有效果——這是初學者最常見的錯誤之一。
另外,int& 只能接變數:addOne(5);、addOne(a + 1); 都會編譯錯誤,訊息裡會出現 cannot bind non-const lvalue reference。lvalue(左值)在這門課先理解成「有名字、住在記憶體裡的東西」(變數;之後學的 a[i]、*p 也算),rvalue(右值)是 5、a + 1 這種「算完就丟的暫時值」——暫時值沒有記憶體位置可以被取小名,所以 int& 只收前者。(正式定義比這複雜:例如 const int x = 1; 的 x 是左值卻不能被指派,本學期不深究。)
混合傳值與傳參考
同一個參數列裡,傳值與傳參考可以混著用。而且參考參數跟 return 不衝突——可以一邊改呼叫端的變數,一邊用回傳值回報狀況:
1 |
|
輸出:
1 | 100 1 |
score 真的被改到了(傳參考),同時用回傳值告訴呼叫者「有沒有撞到上限」——輸出的 1 就是 true(09/17 提過,bool 印出來是 1/0)。
return 一次只能帶回一個值。要一次帶回兩個結果,就改用兩個參考參數:
1 | void minMax(int a, int b, int& mn, int& mx) { |
呼叫端先準備兩個變數 int lo, hi;,跑完 minMax(7, 3, lo, hi); 之後 lo 是 3、hi 是 7。
雷區 ①:手滑寫成區域變數
1
2
3
4 void readScore(int& score) {
int score2 = score; // 想先留一份原值,變數卻取了個跟參數只差一字的名字
cin >> score2; // 想讀進 score,卻讀進了 score2;外面的 score 完全沒變
} // 函式結束,score2 消失課本稱為 inadvertent local variable:編譯完全不會報錯,但外面的變數一輩子不會變,症狀是「函式明明跑了,值卻沒變」。同名(
int score)編譯器會直接擋下來(shadows a parameter),所以真正會出事的永遠是score2這種只差一個字的名字。
const 參考:又快又安全
1 |
|
輸出:
1 | hello |
複製一個長字串很花時間,& 省掉複製,const 保證不會動到呼叫者的東西。順帶回收前面那條規則:int& 不能接 5 這種暫時值,但 const 型別& 可以——因為保證不會改它,編譯器乾脆替暫時值找塊地方放著,所以上面的 print("literal") 才合法。規則:只讀不改、又比一個 int 大的,一律用 const 型別&(之後學到的 struct、vector 都適用;陣列是例外——它傳進函式時本來就不會複製,10/08 會解釋為什麼)。
三種傳法總整理
| 情況 | 寫法 |
|---|---|
只讀取,資料很小(int、double、char) |
傳值 int x |
| 要改到呼叫端的變數,或一次帶回兩個以上的結果 | 傳參考 int& x |
只讀取,但資料比一個數字大(string 等) |
傳 const 參考 const string& s |
函式重載(overloading)
同一個函式名字,只要參數列表不同(型別不同或個數不同),就可以同時存在:
1 |
|
輸出:
1 | 7 |
編譯器根據你傳進去的引數型別與個數決定要呼叫哪一個版本,這個挑選過程叫重載解析(overload resolution)。(有些書把 overloading 譯成「多載」,是同一件事。)
先分清楚兩個詞:提升=往同族、更大、不會失真的型別搬(char/short→int、float→double);轉換=跨族或可能失真(int→double、double→int、int→long)。編譯器偏好提升多過轉換,挑選順序大致是:
- 找完全吻合的版本。
- 找只需要提升就能吻合的。
- 找需要轉換才能吻合的。
- 都找不到或同時有兩個一樣好→ 編譯錯誤
call of overloaded ... is ambiguous。
雷區 ②:只有回傳型別不同,不算重載
1
2 int f(int x);
double f(int x); // 編譯錯誤因為呼叫
f(3);時編譯器無從判斷你要哪一個。
雷區 ③:兩邊都要轉型,又一樣好 → 模稜兩可
1
2
3 void show(long x); // long 是另一種整數型別,大小介於 int 與 long long 之間
void show(double x);
show(5); // 編譯錯誤:int→long 和 int→double 都是「轉換」,一樣好g++ 會報
call of overloaded 'show(int)' is ambiguous。對照一下:如果兩個版本是show(int)與show(double),那show('A')不會有事,因為char→int是型別提升(第 2 順位),贏過轉型(第 3 順位)。寫重載時讓參數型別差異明顯。
預設引數(default arguments)
在參數後面寫 = 值,呼叫時就可以省略這個參數,省略時自動用這個值:
1 |
|
輸出:
1 | Hello, world! |
注意這裡順手用了 const string&——參數是參考,一樣可以給預設值。
規則:
- 有預設值的參數必須放在參數列最右邊(不然呼叫時無法判斷你省略了哪一個)。
- 預設值只能寫一次:有函式原型就寫在原型,沒有原型就寫在定義;兩邊都寫 g++ 會報
default argument given for parameter 2 ... previous specification。
雷區 ④:預設引數會跟重載打架
同時有void g(int a);和void g(int a, int b = 1);時,g(5);兩個都吻合,編譯錯誤call of overloaded 'g(int)' is ambiguous。
用 assert 檢查前置條件
Ch4 後半換個主題:函式寫多了,怎麼確認它真的對。先從 09/24 那行寫在註解裡的 precondition(前置條件)講起——assert 可以讓那行註解自動被檢查。它是「這件事一定要成立,不成立就讓程式當場停下來」的除錯工具,要 #include <cassert>:
1 |
|
輸出(在終端機直接執行):
1 | 2.5 |
第一行要注意:divide(10, 4) 印的是 2.5 不是 2。10、4 雖然是整數字面值,但參數型別是 double,傳進去的瞬間就先轉成 10.0 和 4.0,所以函式裡算的是 10.0 / 4.0,不是 09/17 的整數除法——引數傳給參數時一樣會做隱式轉換,這正是上面重載解析第 3 順位在講的事。
後兩行就是斷言失敗的樣子:它直接告訴你哪個檔、第幾行、哪個條件掛掉,比自己到處插 cout 找 bug 快得多。
編譯時加上 -DNDEBUG 就會把所有 assert 關掉(正式版不做檢查):
1 | g++ -std=c++11 -DNDEBUG -o app main.cpp |
(-D 是「編譯時定義一個名字」,NDEBUG 是標準規定的固定名字。)實驗課寫作業時放幾個 assert 很好用,但繳交前確認它不會誤觸發。
測試技巧:stub 與 driver
- Stub(樁):某個函式還沒寫好,先給一個假的實作,讓其他部分能先編譯、先測。
- Driver(驅動程式):寫一個小
main專門測試某一個函式,測完再把它接回大程式。
stub 長這樣:
1 | // stub:故意亂答(連 4 都會說是質數),只是為了讓用到它的程式先編譯、先跑起來 |
等外圍都測通了,再換成真正的實作,並配一個 driver 逐一檢查:
1 |
|
輸出:
1 | 1 -> not prime |
這兩個名詞筆試可能考定義,實務上也真的好用:與其整支寫完才編譯,不如寫一個函式測一個。
本週重點回顧
- 有
&的參數是別名,在函式裡改它就是改到外面;沒有&的參數是複本,怎麼改都動不到呼叫端——這是筆試最愛考的觀念。兩個補充:const 型別&有&但被禁止改;11/19 學指標後會看到「指標本身是複本,但透過它仍能改到指向的東西」。另外int&只能接變數。 - 只讀不改又比一個數字大的東西,一律用
const 型別&傳:不複製、又保證不被改。 - 重載看的是參數列表(型別或個數),回傳型別不同不算重載,會編譯錯誤。
- 預設引數只能放在參數列最右邊,而且原型與定義只能擇一寫。
assert(條件)把前置條件變成會自動檢查的程式碼(要#include <cassert>);stub 是暫時的假實作,driver 是專門測一個函式的小main。
本週練習題
Q1. 交換與排序三個數
寫 void swapValues(int& a, int& b),再用它把讀入的三個整數由小到大排序輸出。
1 | 輸入: 5 2 9 |
參考解答
1 |
|
Q2. 一次回傳最大值與最小值
寫 void minMax(int a, int b, int c, int& mn, int& mx),用參考參數把三個數的最小值與最大值帶回呼叫端。
1 | 輸入: 7 3 10 |
參考解答
1 |
|
為什麼要這樣寫? 跟正文的兩數版一樣,只是候選人從兩個變三個。
Q3. 重載 area
寫三個同名函式 area:
- 一個參數 → 正方形面積
- 兩個參數 → 長方形面積
- 三個參數 → 三角形面積(海龍公式)
海龍公式:$s = \frac{a+b+c}{2}$,面積 $= \sqrt{s(s-a)(s-b)(s-c)}$
1 | 輸入: 3 4 5 |
參考解答
1 |
|
Q4. 帶預設值的利息計算
寫 double compound(double principal, double rate = 0.02, int years = 1),計算複利本利和 $P(1+r)^n$,並示範三種呼叫方式。
1 | 輸入: 10000 |
參考解答
1 |
|
Q5. 綜合題:加分函式
寫 void applyBonus(const string& name, int& score, int bonus = 5):把 score 加上 bonus(上限 100),再印出名字與加分後的成績;函式開頭用 assert 擋掉負的 bonus。另外寫一個 driver main 測三組資料,其中一組省略 bonus、一組要撞到上限。
1 | 輸出: |
參考解答
1 |
|
一題用上本週四個重點:const string&(只讀不改的大東西)、int&(要改到呼叫端)、預設引數、assert 前置條件,外加一個 driver。
實驗課題型加練
以下三題的題型取自去年(2025)第 4 週實驗課的課堂練習(今年這週的投影片還沒出,題目可能會換):同一個 swap 寫傳值版與傳參考版、預設引數、還有一題「反轉相加到迴文」的經典題。題目不是我原創的——練的東西跟去年那幾題一樣,但敘述、規則細節、範例資料和解答都是我自己重寫的,不是原題。Q9 的題型取自歷年考古題(連續輸入、讀到 0 0 結束的最大公因數與最小公倍數),Q10 配合課本勾選題。
Q6. 傳值與傳參考:眼見為憑
寫兩個交換函式 swapByValue(int a, int b) 與 swapByRef(int& a, int& b),函式內交換後各印一次,main 在呼叫前後也各印一次,觀察差別。
1 | 輸入: 3 8 |
參考解答
1 |
|
函式裡兩個版本印出來一模一樣,差別只在回到 main 之後:傳值版改的是複本,x、y 沒動;傳參考版的 a、b 就是 x、y 的別名,所以真的換了。這題實驗課助教常常追問「為什麼裡面印的一樣、外面不一樣」,要能用「複本 vs 別名」講出來。
Q7. 反轉相加到迴文
把一個數跟它的反轉相加,結果若不是迴文就再做一次,直到出現迴文。例如 87 → 87 + 78 = 165 → 165 + 561 = 726 → 726 + 627 = 1353 → 1353 + 3531 = 4884(迴文)。讀入起始數字,印出每一步、總共加了幾次、最後的迴文;超過 10 次還沒出現就放棄。要求兩個函式:void reverseDigits(long long n, long long& result) 用傳參考帶回反轉結果,bool isPalindrome(long long n) 判斷迴文。
1 | 輸入: 87 |
1 | 輸入: 89 |
參考解答
1 |
|
反轉的邏輯跟 09/24 的 Q3 一樣,只是結果改用參考參數帶回、回傳型別變成 void——這是題目指定的練習重點。用 long long 是因為數字每加一次就長一位,int 撐不到 10 步(89 那組第 10 步已經是七位數再反轉相加)。
Q8. 字尾符號(預設引數)
讀入一個不含空白的字串,用同一個函式 void decorate(const string& s, int len = 0) 印兩次:第一次不給長度,一律在尾端加 .;第二次把字串長度傳進去,長度 ≥ 6 加 !!!、2–5 加 !、1 加 ?。字串長度用 s.size() 取得(string 內建的功能,回傳字元數;10/29 會系統性介紹)。
1 | 輸入: makefile |
1 | 輸入: hey |
1 | 輸入: k |
參考解答
1 |
|
預設引數 = 0 讓「沒給長度」變成一個可以判斷的狀態——真正的長度不可能是 0(cin >> s 讀到的字串至少有一個字元),所以 0 可以放心拿來當「沒給」的記號。decorate(s, s.size()) 把 size_t(無號)傳給 int 參數會發生隱含轉換,g++ 在 -Wall -Wextra 下不會警告,而且字串長度不可能大到出問題;想寫得更明確可以用 static_cast<int>(s.size())。
Q9. 最大公因數與最小公倍數
反覆讀入兩個正整數,輸出它們的最大公因數與最小公倍數,讀到 0 0 結束;有非正數就印 invalid。最大公因數用輾轉相除法(09/24 正文寫過的迴圈版):反覆把 $(a, b)$ 換成 $(b, a \bmod b)$,直到 $b$ 變成 0,此時 $a$ 就是答案。
1 | 輸入: |
參考解答
1 |
|
輾轉相除法的迴圈每圈做一次「換成 (b, a % b)」,b 變 0 就停,跟 09/24 正文那份一樣,只是型別改成 long long 裝得下範例的大數。最小公倍數寫成 a / gcd * b 而不是 a * b / gcd:先乘的話 a * b 可能先溢位,就算最後除回來也已經錯了。「讀到 0 0 結束」是實驗課上機考的標準格式,寫成 while (true) 加 break 最直白。
Q10. 換算選單:輸入、計算、輸出各一個函式
反覆讀入選項:1 攝氏轉華氏、2 華氏轉攝氏(選完再讀一個溫度)、0 結束,其他印 unknown choice。規定拆成四個函式:readTemp() 讀一個溫度並回傳、cToF(double)、fToC(double)、printResult(...) 負責印,main 只做選單。結果印到小數一位。
1 | 輸入: |
參考解答
1 |
|
課本 Ch4 有一組連續三題就是這個骨架:先各寫一個方向的換算,再用 if-else 把兩個方向合成選單,最後包進「重複到使用者結束」的迴圈。main 裡每個分支只有兩行——讀、算+印——因為工作都拆給函式了。
說些什麼吧!