先讀語法與例子,再操作互動圖解並完成四選一自測。忘記寫法時回看速查表,最後用中英詞彙卡複習。
上一頁的敘述大多由上往下執行。這頁加入兩種能力:用 if 或 switch 依條件選擇敘述;用 while 或 for 重複敘述。先看各形式適合什麼情況,接著逐一拆開括號、條件、大括號與更新動作。
| 結構 | 最小外觀 | 什麼時候執行 | 適合情境 |
|---|---|---|---|
| if | if (ready) { … } | 條件為真時執行 | 依條件決定是否處理 |
| switch | switch (choice) { case 1: …; break; } | 跳到相符標籤後執行 | 同一個整數有數個固定選項 |
| while | while (remaining > 0) { … } | 每次先檢查條件 | 重複到條件不成立 |
| for | for (int i = 0; i < 3; ++i) { … } | 初始化後反覆檢查與更新 | 明確的計數或走訪 |
先辨認你要的是「選擇」還是「重複」,再看條件在什麼時候檢查。條件與更新要一起讀:同一個 while 外觀,搭配不同的更新動作,就可能執行不同次數。下面逐節示範,最後再用巢狀迴圈比較次數。
C++ 的流程控制長得跟 Python 很像,但有四個地方需要特別留意:大括號、switch 的掉落、三段式 for 的執行時機,以及範圍 for 中決定「改得動還是改不動」的 &。這四個各有一節。
▲ 三種結構就是全部了:循序一行接一行往下走;選擇在菱形處分岔,兩條路走完會合成一條;迴圈的菱形多一條往回跳的線,條件成立就再繞一圈,不成立才從旁邊離開。本頁後面每一節都在講這三張圖裡的某一個角落。
本頁整理後續程式常用的流程控制。像 divisor != 0 && numerator / divisor > 3 這類保護條件為什麼不能顛倒,答案就在 PART 01。
bool、六個比較運算子,以及避免無效運算的短路求值C++ 的 bool 只有兩個值:true 與 false。轉成整數時分別得到 1 與 0;整數轉成 bool 時,0 為假、非零為真。這是型別轉換規則,不能據此推定 bool 的底層表示方式。
a = true,b = false 3 < 5 -> true 3 == 5 -> false 3 != 5 -> true a && b -> false a || b -> true !a -> false true 印成數字是 1,false 是 0 非零整數當條件時是真:(bool)42 = 1 空字元 '\0' 轉成 bool = 0
boolalpha 這個操作子讓 cout 把 bool 印成 true/false,不加的話印出來是 1 與 0。最後兩行是重點:42 轉成 bool 是 1,'\0' 轉成 bool 是 0,也就是說 C++ 的條件不必是 bool,是整數就行。
| 運算子 | 唸法 | 要注意什麼 |
|---|---|---|
| == != | 相等、不相等 | 比較用兩個等號。一個等號是賦值 |
| < <= > >= | 大小比較 | 基本型別已有比較規則;自訂型別的比較可以另行定義,詳見 P9。 |
| && | 而且(and) | 左邊為假就不看右邊 |
| || | 或者(or) | 左邊為真就不看右邊 |
| ! | 否定(not) | Python 寫 not,C++ 寫驚嘆號 |
&& 的右邊不一定會被執行這是本頁最該記住的一件事。a && b 求值時,C++ 會先算 a;如果 a 已經是假,整個式子不可能為真,b 就完全不會被求值。|| 反過來:左邊為真就直接收工。
false && 右邊:
左邊求值了
整式結果 false
true && 右邊:
左邊求值了
右邊求值了
整式結果 false
true || 右邊:
左邊求值了
整式結果 true
false || 右邊:
左邊求值了
右邊求值了
整式結果 true四組裡有兩組只印了「左邊求值了」——右邊那個函式連呼叫都沒有發生。這不是最佳化,是 C++ 標準規定的行為,你可以放心依賴它。
短路求值常用來保護只在特定條件下才安全的運算。例如除法前要先確認除數不為 0;若保護條件失敗,右側的除法便不會執行。
條件不成立
divisor != 0 不成立,因此 numerator / divisor 根本沒被求值,程式安全地走進 else。
[未定義行為:整數除以零]
只把 && 兩邊對調,程式便會先計算 numerator / divisor。整數除以零是未定義行為,不能依賴特定錯誤訊息或結束碼。
&& 左右兩邊的順序在這裡不是風格問題,是正確性問題。凡是右側運算有前提,先決條件都要放左邊。例如 i < v.size() && v[i] != x 先檢查索引,才安全讀取元素。
C++ 也支援 and、or、not,分別是 &&、||、! 的替代拼法。本頁使用符號形式;遇到文字形式時依相同規則閱讀即可。
if / else if / else:大括號不是可有可無先記住三個語法位置:條件要用小括號包起來,else if 中間有空格,主體用大括號。省略大括號時,後來新增的敘述可能不在條件控制之內。
C++ 靠大括號決定範圍,縮排對編譯器完全沒有意義。省略大括號時,if 只管接在它後面的那一個敘述,第二行縮排得再整齊也沒有用。
沒加大括號的版本: 我以為這行也在 if 裡面 加了大括號的版本: (第二段什麼都沒印,因為 x = -5)
x 是 −5,第一段的 if 明明不成立,「我以為這行也在 if 裡面」還是印出來了。第二段加了大括號,兩行就一起被跳過。這個 bug 難抓的地方在於:程式看起來完全正常,只有在條件不成立時才會露餡。
永遠寫大括號。即使現在只有一行,你或別人之後加第二行時就會踩到。歷史上最有名的案例是 2014 年 Apple 的 goto fail —— 一個沒有大括號的 if 底下多了一行重複的 goto,SSL 憑證驗證整個失效。
if (x = 5) 為什麼編得過Python 會直接給你語法錯誤,C++ 不會。理由在 PART 01 已經鋪好了:賦值本身是一個運算式,它的值就是被賦進去的那個值,而任何非零整數當條件都算真。
進到 if 裡面了,而且 x 已經被改成 5 y 不等於 5,y 還是 3
x = 5 這個運算式的值是 5,5 不是零,所以條件成立,而且 x 被順手改掉了。g++ 預設不會警告;加上 -Wall 才會提醒你 suggest parentheses around assignment。如果寫成 if (x = 0),條件就永遠不成立——同樣沒有任何錯誤訊息。
使用 -Wall -Wextra 可協助發現可疑賦值。條件裡出現單一 = 時,要確認自己要的是「改值」還是 == 的「比較」;編譯成功不代表意圖正確。
?::一行選一個值的 if/elsecond ? a : b 是「有值的 if/else」:cond 成立整個式子就變成 a,不成立就變成 b。它做的事跟上面的 if/else 一模一樣,差別是它本身就是一個值,可以直接拿去指派、印出、或當函式的參數,不用像 if/else 那樣先宣告一個變數,兩邊分支再各指派一次。
15.6667 0
三元運算子只求值被選中的分支。count0 為 0 時會選左側的 0.0,右側除法不會執行;count 為 3 時則選右側。因為 total 與 count 都是 int,範例先將 total 轉成 double,保留除法的小數部分。
| 寫法 | C++ 程式碼 |
|---|---|
| 完整 if/else | if (count == 0) average = 0.0; else average = static_cast<double>(total) / count; |
| 三元寫法 | average = count == 0 ? 0.0 : static_cast<double>(total) / count; |
兩種寫法在這個例子中得到相同結果;三元運算式本身會產生一個值,因此適合在兩個簡單值之間選擇。若每條分支需要多個敘述,使用完整的 if/else 會更清楚。
int x = (5 > 3) ? 10 : 20; 之後 x 是多少?
switch 與掉落:Python 沒有的控制流程當你要拿同一個整數或字元去比對一長串固定值時,switch 比一串 else if 好讀。Python 到 3.10 才有 match,而且語意完全不同,所以這一節對你是全新的。
case 後面必須是編譯期就知道的常數,不能放變數、不能放範圍、不能放字串。break 的作用是「跳出這個 switch」,少寫一個,控制流就會直接掉進下一個 case 的主體。
輸入 A -> 很好 不錯 及格 (一路掉到底) 輸入 B -> 不錯 及格 (一路掉到底) 輸入 C -> 及格 (一路掉到底) 輸入 F -> (一路掉到底)
輸入 'A' 時,四段全部被執行了。switch 的 case 是跳進去的位置,不是一個個獨立的分支——找到相符的標籤就從那裡開始往下跑,一路跑到 break 或右大括號為止。這叫掉落(fall-through)。
掉落也可以是故意的。把多個 case 疊在一起、中間什麼都不寫,就是「這幾個值走同一段程式」的標準寫法:
'a' -> 母音 'u' -> 母音 '-' -> 運算子 'z' -> 其他
五個母音的 case 共用一個主體與 break,表示不同輸入採取相同處理。case 是進入位置,離開仍取決於 break 或其他控制流程,不能把每個 case 當成自動互斥的區塊。
switch | 一串 else if | |
|---|---|---|
| 比對的東西 | 只能是整數/字元/列舉 | 任何布林運算式 |
| 能比範圍嗎 | 不行 | 可以,x > 10 && x < 20 |
| 忘記寫收尾 | 掉落,靜靜地跑錯 | 不會,分支天生互斥 |
| 什麼時候用 | 值是一組固定常數(選單、字元分類) | 其他所有情況 |
下面這段的 switch (2) 會印出什麼?
C++17 可用 [[fallthrough]]; 表達刻意接著執行下一個 case 的意圖,讓讀者及編譯器辨認。它不會中止流程;要離開 switch 仍須使用 break。
while:不知道要跑幾圈的時候用它while 每次先檢查條件,成立才執行主體。適合停止時機由資料決定的情境,例如讀取到 quit 才停止,或把數量逐步減到 0。特別檢查主體是否確實改變影響條件的狀態。
while 的主體跑了 0 次 do-while 的主體跑了 1 次
n 一開始就是 10,n < 5 從頭到尾不成立。while 一圈都沒跑,do-while 跑了一圈才發現。do-while 的主體保證至少執行一次,所以它適合「先做一次,再問要不要繼續」,例如讀使用者輸入直到合法為止。
迴圈跑不完只有一個原因:主體沒有把條件推向不成立。下面兩種是最常見的寫法錯誤。
也可以使用 while (true),在讀到特定輸入時以 break 離開;這種結構把停止判斷放在主體中:
另一種更新方式是每圈將剩餘數量除以 2。下例從 40 開始,依序變成 20、10、5、2、1、0。每圈不再只減一,所以執行次數較少;仍然是「先判斷、做主體、更新、再判斷」同一套 while 規則。
第 1 圈:remaining = 40 第 2 圈:remaining = 20 第 3 圈:remaining = 10 第 4 圈:remaining = 5 第 5 圈:remaining = 2 第 6 圈:remaining = 1 共 6 圈,最後剩 0
這個例子從 40 開始,每次做整數除以 2,共執行 6 圈才變成 0。一般而言,正整數每圈至少減半,圈數隨初值的二進位位數成長;這正是從更新式判讀對數成長的方式。
for:三個位置,三個不同的執行時機Python 的 for 是「把可迭代物件的東西一個一個拿出來」。C++ 的三段式 for 不是那個意思,它就是一個 while 的縮寫,只是把三件事排在同一行:
執行順序是這樣的:初始化只在最開始跑一次;接著檢查條件,成立就跑主體,跑完執行更新,再回去檢查條件。若迴圈正常因條件變成假而結束,條件檢查會比主體執行多一次;若以 break、return 或例外提早離開,則不能套用這個計數。
兩段程式碼完全等價。差別只在 for 把 ①②④ 收在同一行,讓讀的人一眼看見迴圈變數的生命週期。
初始化 i = 0 條件成立,主體讀到 i = 0 條件成立,主體讀到 i = 1 條件成立,主體讀到 i = 2 i 更新到 3,條件不成立,結束
把三段各換成一個會印字的函式,順序就攤開了:初始化的字只出現一次,然後是「檢查 → 主體 → 遞增」重複三輪,最後多一次落空的檢查。主體 3 次、條件 4 次——分析複雜度時我們數的是主體那一欄,但別忘了條件那一次也要跑。
| 寫法 | 跑幾圈 | i 走過哪些值 | 常見用途 |
|---|---|---|---|
| for (int i = 0; i < n; i++) | n | 0 … n−1 | 走訪長度 n 的陣列,最標準的寫法 |
| for (int i = 1; i <= n; i++) | n | 1 … n | 從 1 數到 n,數學公式對照時用 |
| for (int i = n−1; i > 0; i--) | n−1 | n−1 … 1 | 從末端倒著走到 1 |
| for (int i = 0; i < n; i += 2) | ⌈n/2⌉ | 0, 2, 4 … | 跳著走 |
| for (int i = n; i > 0; i /= 2) | ⌊log₂n⌋+1 | n, n/2, n/4 … | 每圈砍一半,這是 log |
for (int i = 0; …) 的 i 只在整個 for 敘述內有效,結束後不能從外面讀它。如果要保留最終值,可在 for 前宣告 i。容器 size() 的回傳型別為無號型別,與有號索引比較要留意轉換與警告。
要觀察一段程式在目前環境的耗時,可使用 <chrono>。auto 讓編譯器從初值推導型別;steady_clock 提供適合量測經過時間的時鐘。先記起點,再以終點減起點,最後轉成秒數。
三個地方值得逐行看懂:
① auto:這是本頁第一次正面介紹它,意思是「型別交給編譯器自己推算,我不寫」。steady_clock::now() 回傳的型別全名是 std::chrono::time_point<std::chrono::steady_clock>,沒有人會想每次計時都手打這一長串,auto start = … 讓編譯器自己看右邊的值決定 start 的型別——這裡不是偷懶,是這個型別名字長到非用不可。② steady_clock::now():拿「現在」這一刻的時間點,呼叫兩次、中間夾著要量的程式,前後兩個時間點相減就是經過的時間。③ duration<double>(…).count():兩個時間點相減得到的是一個 duration 物件,單位不是秒;duration<double>(...) 把它轉成「以 double 表示的秒數」,.count() 再把數字取出來變成一個普通的 double。
三次的 total 都是 100010000;seconds 是每次計算經過的秒數。
用 steady_clock::now() 記下開始與結束時間,兩者相減得到經過時間,再用 duration<double> 換算成秒。程式重複計算三次,每次都印出總和與耗時;工作很短時可能量到 0。
<chrono> 要明確 include使用 steady_clock 與 duration 前應寫 #include <chrono>,不要依賴其他標頭間接帶入宣告。若省略,編譯器通常會回報名稱未宣告。
for (T x : v):最像 Python 的那個 for,也最容易寫錯vector<int> 與 int&vector<int> v = {1, 2, 3}; 建立一個可依序走訪的整數容器。int& x 表示 x 是某個既有整數的別名;經由 x 修改數值,也會改到原本的整數。下方範圍 for 正是用這兩項語法比較「複本」與「別名」。
C++11 的範圍 for 可依序走訪容器中的每個元素。以下用剛介紹的 vector<int> 保存數個整數,比較讀取副本與透過參考修改元素的不同。
| 寫法 | x 是什麼 | 能改到容器嗎 | 會複製嗎 | 什麼時候用 |
|---|---|---|---|---|
| for (T x : v) | 每個元素的複本 | 不能 | 會,每圈一份 | 元素很小(int、char)且只是讀 |
| for (T& x : v) | 每個元素的別名 | 能 | 不會 | 要修改元素 |
| for (const T& x : v) | 唯讀的別名 | 不能 | 不會 | 元素很大又只是讀,例如 string、物件 |
上表的 T 可以換成 auto,從容器推導元素型別。for (auto x : v) 仍建立副本,for (auto& x : v) 仍建立別名,for (const auto& x : v) 仍以參考唯讀走訪。auto 省下型別名稱,範圍 for 省下逐步走訪的寫法,& 則決定是否參考原元素。完整對照見 P6:auto 與範圍 for 的語法糖。
差別就是一個 &。沒有它,你改的是一份用完就丟的複本;有了它,你改的是容器裡的本尊。
原始 : 1 2 3 for (int x) : 1 2 3 for (int &x) : 10 20 30
中間那一圈 x *= 10 明明執行了三次,v 卻一點都沒變——因為 x 每圈都是一份新的複本,改完就被丟掉。加上 & 之後同一行才真的動到 v。g++ 對第一種寫法連警告都不會給,這個 bug 只能靠自己看出來。
要修改元素時用 T&。只讀取 int 這類小型別時,直接用 T 清楚且成本低;只讀取 string、vector 或自訂大型物件時,用 const T& 避免複製。
複製與參考的差別也會出現在函式參數:複製大型容器可能昂貴,唯讀參考可以避免整份複製。這裡先掌握 range-for 的元素別名,下一頁 P3 再從函式宣告學到參數傳遞。
假設外層固定執行 n 次,而且每一圈內層也完整執行 n 次,內層主體共 n × n 次。若內層上限隨外層變動,就逐圈加總。先數這些具體次數,再談成長速度;迴圈形狀本身不能取代對每圈工作量的檢查。
方形雙層 n = 8:內層跑 64 次,等於 n * n 逐輪減少的雙層 n = 8:內層跑 28 次,等於 n(n-1)/2 n 變兩倍,比較次數大約變四倍: n = 8 -> 28 次 n = 16 -> 120 次 n = 32 -> 496 次 n = 64 -> 2016 次 n = 128 -> 8128 次
完整方形共 n × n = 64 次;三角形版本的內層上限隨外層縮短,共 n(n−1)/2 = 28 次,少於方形的一半。兩者在 n 加倍時都約成長四倍;假設每次主體工作量固定,便都屬於 O(n²)。
break 與 continue:提早離開的兩種方式break 是「這個迴圈我不跑了」,continue 是「這一圈剩下的不做了,直接進下一圈」。兩個都只作用在最靠近的那一層迴圈。
17 是奇數,continue 跳過剩下的主體 93 是奇數,continue 跳過剩下的主體 檢查 26 找到了,break 離開迴圈 break 只跳出「最內層」那一圈: i = 0,j = 0 i = 1,j = 0 i = 2,j = 0
第二段內層在 j == 1 時 break,只離開當前內層迴圈,外層仍執行三次。要離開多層可用額外旗標,在每層檢查是否停止;學過函式後也可用 return 離開整個函式。
| 迴圈形狀 | 次數 | 複雜度 | 使用情境 |
|---|---|---|---|
| for i < n | n | O(n) | 依序處理 n 筆資料 |
| for i < n { for j < n } | n² | O(n²) | 走訪二維陣列 |
| for i = n−1 … { for j < i } | n(n−1)/2 | O(n²) | 逐輪少做一次的配對工作 |
| while (範圍砍一半) | log₂n | O(log n) | 二分搜尋 |
| 外層 n 次 { 內層每次減半 } | n log₂n | O(n log n) | 重複執行減半流程 |
下面這段程式印出什麼?
把上面那段貼進 C++ Tutor,然後做三個改動:① 把 int x 改成 int &x;② 把 x = 0 改成 x == 0;③ 兩個一起改。三次的輸出都不一樣,能講出為什麼,這一頁就讀完了。
while | 三段式 for | 範圍 for | |
|---|---|---|---|
| 什麼時候用 | 不知道要跑幾圈 | 要用到索引 i | 只是把元素一個個拿出來 |
| 主體保證跑一次嗎 | 不保證(do-while 保證) | 不保證 | 容器空的話一次都不跑 |
| 最常忘記什麼 | 更新條件變數 | 邊界寫成 <= 而越界 | 那個 & |
| 適合情境 | 重複到輸入有效或剩餘量歸零 | 固定次數與索引走訪 | 逐一讀取容器元素 |
| 你在 Python 寫的 | C++ 對應 | 差在哪 |
|---|---|---|
| if x > 0: | if (x > 0) { … } | 條件要小括號,主體要大括號 |
| elif | else if | C++ 是兩個字,中間有空格 |
| and / or / not | && / || / ! | 兩邊都有短路求值 |
| match / case | switch / case | C++ 會掉落,要自己寫 break |
| while cond: | while (cond) { … } | 一樣,只差括號 |
| 沒有 do-while | do { … } while (cond); | 主體保證至少跑一次 |
| for i in range(n): | for (int i = 0; i < n; i++) | C++ 把三段攤開給你控制 |
| for x in v: | for (int x : v) | C++ 預設是複本,要改要加 & |
| for i, x in enumerate(v): | for (unsigned i = 0; i < v.size(); i++) | C++ 沒有內建的 enumerate |
| break / continue | break / continue | 一樣,都只作用在最內層 |
| for … else | 沒有對應 | 用旗標變數自己做 |
① 條件裡「先檢查指標、再解參考」,順序不能顛倒,靠的是 && 的短路求值。
② 永遠寫大括號,即使主體只有一行。
③ 每個 case 寫完就問一次:這裡要不要 break。
④ 迴圈寫完就問一次:主體有沒有把條件推向不成立。
⑤ 範圍 for 要改元素就加 &,只讀又是大東西就寫 const T&。
| 資源 | 看什麼 |
|---|---|
| C++ Tutor | 把本頁每一段迴圈貼進去單步執行,看變數怎麼變、條件什麼時候被檢查。 |
| cppreference · range-for | 範圍 for 展開之後的真正樣子,以及為什麼 & 會改變行為。 |
| cppreference · switch | 掉落的完整規則,以及 C++17 的 [[fallthrough]]。 |
| cppreference · for statement | 三段式與範圍 for 的正式語法參考。 |
| 本站 · 演算法分析 | 延伸本頁 PART 07,正式介紹漸近複雜度與成長率。 |
| cppreference · break statement | break 在迴圈與 switch 中的正式語法與作用範圍。 |
每個選項都有解說:選錯也點開看看為什麼錯。全對之後再往下翻詞彙卡。
#include <iostream>
using namespace std;
int main() {
int sum = 0;
for (int i = 1; i <= 4; i++) {
sum += i;
}
cout << sum << endl;
return 0;
}
#include <iostream>
using namespace std;
int main() {
int n = 2;
switch (n) {
case 1: cout << "one ";
case 2: cout << "two ";
case 3: cout << "three ";
break;
case 4: cout << "four ";
}
cout << endl;
return 0;
}
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3};
for (int x : v) x += 10;
for (int &y : v) y += 100;
for (int z : v) cout << z << " ";
cout << endl;
return 0;
}
#include <iostream>
using namespace std;
int main() {
int i = 5;
int count = 0;
do {
count++;
i++;
} while (i < 3);
cout << count << endl;
return 0;
}
#include <iostream>
using namespace std;
int main() {
int hits = 0;
for (int i = 0; i < 5; i++) {
for (int j = i + 1; j < 5; j++) {
hits++;
}
}
cout << hits << endl;
return 0;
}
先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。