C++ 流程控制:讓程式自己決定走哪條路
從 if/else、switch 到 range-based for,掌握 C++ 流程控制的語言特色,並從編譯器與 CPU 的視角理解分支預測與迴圈最佳化
讓程式自己決定走哪條路
想像你在寫一支判斷成績等第的小工具:分數 90 以上是 A、80 到 89 是 B、不及格還要印出警告。程式不能只會「從上往下一行一行讀」,它得學會看情況轉彎、重複做某件事直到完成。這就是流程控制(control flow)。
如果你讀過本專區的 Python 篇,這些概念對你不陌生——條件、迴圈、跳出。但 C++ 把同樣的概念交給一個靜態型別、會編譯成機器碼的語言時,故事就變得不太一樣了。在 Python 裡 if 後面接什麼幾乎都能跑;在 C++ 裡,編譯器會在你按下執行之前,先把你的條件式「攤開來檢查型別」,甚至幫你預測「這個 if 比較可能成立還是不成立」,再決定產生什麼樣的機器指令。
這篇文章帶你把 C++ 的流程控制寫熟,同時看清楚它跟動態語言在底層的根本差異。我們會大量動手寫可以直接編譯執行的程式碼,請打開你的編譯器(g++ 或 clang++)跟著敲。

if / else:條件是「布林」,不是「真假值」
最基本的分支從 if 開始。先看一段完整可編譯的程式:
#include <iostream>
int main() {
int score = 76;
if (score >= 90) {
std::cout << "等第 A\n";
} else if (score >= 60) {
std::cout << "等第 B(及格)\n";
} else {
std::cout << "不及格,請加油\n";
}
return 0;
}
// 輸出:等第 B(及格)
語法上跟很多語言一樣,但有幾個 C++ 的眉角值得停下來看。
第一,條件式的型別是 bool。 score >= 90 這個運算式的結果型別是 bool,只有 true 或 false 兩個值。相較於 Python 用「真假值(truthiness)」把空字串、0、None 一律當成假,C++ 沒有這套規則:能放進 if 的,是任何「可以轉成 bool」的東西。整數 0 會轉成 false、非零轉成 true,但這是隱式型別轉換,不是 truthiness。
第二,當心 = 和 ==。 這是 C/C++ 初學者最經典的雷:
int x = 5;
if (x = 0) { // 反模式:這是「賦值」,把 0 指派給 x,整個式子的值是 0(false)
std::cout << "永遠不會印\n";
}
x = 0 是賦值,它的結果是被賦的值 0,再被當成條件——於是永遠是 false,而且 x 還被你偷偷改掉了。正確的相等比較是 ==。現代編譯器加上 -Wall 會對這種寫法發出警告,請務必開啟:
g++ -Wall -Wextra -std=c++17 main.cpp -o main
第三,大括號別省。 C++ 允許 if 後面只接單一句而省略 {},但這會埋下著名的「goto fail」類錯誤。請養成永遠寫大括號的習慣。
switch:跳表式的多路分支
當你要對「同一個整數型變數的多個離散值」分別處理時,switch 比一長串 else if 更清楚,而且編譯器有機會把它最佳化成跳轉表(jump table),達到接近 $O(1)$ 的分派:
#include <iostream>
int main() {
int menu = 2;
switch (menu) {
case 1:
std::cout << "新增資料\n";
break;
case 2:
std::cout << "查詢資料\n";
break;
case 3:
std::cout << "刪除資料\n";
break;
default:
std::cout << "未知選項\n";
break;
}
return 0;
}
// 輸出:查詢資料
break 是必要的,漏掉會「貫穿(fall-through)」。 這是 C++ 跟 Python(沒有 switch,但有 match)很不一樣的地方:少了 break,程式會繼續往下一個 case 執行,直到遇到 break 或 switch 結束。貫穿偶爾是刻意設計(多個 case 共用同一段邏輯),但更多時候是忘了寫 break 的 bug。C++17 起可以用 [[fallthrough]]; 屬性明確標示「我是故意的」:
switch (grade) {
case 'A':
case 'B': // A 和 B 共用:刻意貫穿,無程式碼即合法
std::cout << "表現優異\n";
break;
case 'C':
std::cout << "尚可,";
[[fallthrough]]; // 明示故意貫穿,編譯器不警告
case 'D':
std::cout << "建議加強\n";
break;
default:
std::cout << "無效等第\n";
}
switch 只能用在整數型(含 char、enum),不能直接 switch 字串——這又是靜態型別與底層思維的結果:跳轉表需要的是「可以當索引的整數」。
for 迴圈:經典三段式
for 是 C++ 最常用的計次迴圈,三個區段以分號隔開:初始化、條件、更新。
#include <iostream>
int main() {
int sum = 0;
for (int i = 1; i <= 10; ++i) { // i 的作用域只在這個迴圈內
sum += i;
}
std::cout << "1 到 10 的總和 = " << sum << "\n";
return 0;
}
// 輸出:1 到 10 的總和 = 55
注意 int i = 1 把計數變數宣告在 for 內部,它的作用域(scope)就只在這個迴圈,迴圈結束就消失。這是好習慣:避免變數外洩污染外層。也請偏好 ++i(前綴遞增)而非 i++;對於基本型別兩者效能相同,但對複雜的迭代器型別,前綴版可省下一次拷貝,寫成習慣不吃虧。
相較於 Python 的 for i in range(1, 11),C++ 的三段式更貼近底層:你親手控制起點、終止條件與步進,編譯器幾乎能一對一翻成「比較 + 條件跳轉」的機器指令。
while 與 do-while:條件先行 vs. 至少做一次
當迴圈次數不固定、由條件決定時,用 while:
#include <iostream>
int main() {
int n = 27;
int steps = 0;
// Collatz 序列:奇數 *3+1,偶數 /2,直到變成 1
while (n != 1) {
n = (n % 2 == 0) ? n / 2 : n * 3 + 1; // 三元運算子
++steps;
}
std::cout << "經過 " << steps << " 步抵達 1\n";
return 0;
}
// 輸出:經過 111 步抵達 1
while 是「先檢查條件,再決定要不要進迴圈」。如果你需要「先做一次、再檢查要不要重複」,就用 do-while——它至少執行一次,很適合「輸入驗證」這類情境:
#include <iostream>
int main() {
int age;
do {
std::cout << "請輸入年齡(0~120):";
std::cin >> age;
} while (age < 0 || age > 120); // 不合法就再問一次
std::cout << "已收到年齡:" << age << "\n";
return 0;
}
別忘了 do-while 結尾的分號,漏掉是常見編譯錯誤。
range-based for:C++11 給你的現代寫法
從 C++11 起,遍歷容器有了更簡潔的語法,概念上最接近 Python 的 for x in list:
#include <iostream>
#include <vector>
#include <string>
int main() {
std::vector<std::string> fruits = {"蘋果", "香蕉", "芭樂"};
// 唯讀遍歷:用 const 參考避免拷貝
for (const std::string& f : fruits) {
std::cout << f << " ";
}
std::cout << "\n";
// 需要修改元素時:用非 const 參考
std::vector<int> nums = {1, 2, 3, 4};
for (int& x : nums) {
x *= 10;
}
for (int x : nums) {
std::cout << x << " ";
}
std::cout << "\n";
return 0;
}
// 輸出:蘋果 香蕉 芭樂
// 輸出:10 20 30 40
這裡藏著 C++ 與動態語言最深刻的差別:參考(reference)與拷貝(copy)的選擇是你的責任。
for (std::string f : fruits):每次迭代都拷貝一份字串,浪費效能。for (const std::string& f : fruits):用唯讀參考,零拷貝,這是遍歷大型物件的慣例寫法。for (int& x : nums):用可寫參考,迴圈內的修改會真的改到容器裡的元素。
在 Python 裡你不需要、也無法做這種區分,因為一切都是物件參考;C++ 把這個控制權還給你,代價是你得想清楚要不要拷貝。這正是「零成本抽象(zero-cost abstraction)」哲學的體現:方便的語法不該偷偷產生你看不見的開銷。
C++17 起還能搭配結構化綁定(structured bindings)遍歷 map:
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> scores = {{"小明", 88}, {"小華", 92}};
for (const auto& [name, point] : scores) { // 同時取出 key 與 value
std::cout << name << ":" << point << " 分\n";
}
return 0;
}
// 輸出:小明:88 分
// 輸出:小華:92 分
break 與 continue:跳出與跳過
break 立刻離開最內層迴圈;continue 跳過本次剩餘的程式碼、直接進入下一輪:
#include <iostream>
int main() {
// 找出 1~100 中第一個能同時被 7 和 11 整除的數
for (int i = 1; i <= 100; ++i) {
if (i % 7 != 0) continue; // 不是 7 的倍數就跳過
if (i % 11 == 0) {
std::cout << "找到了:" << i << "\n";
break; // 找到就離開
}
}
return 0;
}
// 輸出:找到了:77
要注意 C++ 沒有 Python 的 for...else,也沒有帶標籤的 break(Java 有)。如果你要一次跳出多層巢狀迴圈,慣用的乾淨做法是把那段邏輯抽成函式、用 return 跳出,或在較罕見情況下審慎使用 goto(多數風格指南不建議)。
邏輯運算子與短路求值
&&(且)、||(或)、!(非)組合多個條件。C++ 對 && 與 || 採短路求值(short-circuit evaluation):
#include <iostream>
#include <vector>
bool isValid(const std::vector<int>& v, int idx) {
// 短路:先確認 idx 在範圍內,才會去存取 v[idx]
return idx >= 0 && idx < (int)v.size() && v[idx] > 0;
}
int main() {
std::vector<int> data = {5, -3, 8};
std::cout << std::boolalpha; // 讓 bool 印成 true/false
std::cout << isValid(data, 0) << "\n"; // 5 > 0
std::cout << isValid(data, 1) << "\n"; // -3 > 0 為假
std::cout << isValid(data, 9) << "\n"; // 越界,但短路擋下了
return 0;
}
// 輸出:true
// 輸出:false
// 輸出:false
短路的關鍵價值在這個例子很明顯:idx < v.size() 為假時,&& 右邊的 v[idx] 根本不會被執行,於是不會發生越界存取。這個順序不能顛倒——把邊界檢查放後面就會踩記憶體地雷。短路不只是效能小技巧,常常是正確性的防線。
請避免把 &&/|| 跟位元運算子 &/| 搞混:後者是逐位元運算、不會短路,兩邊都會求值,語意完全不同。
動手寫一段:質數判斷器
把目前學到的東西組合起來,寫一支判斷使用者輸入數字是否為質數的完整程式:
#include <iostream>
bool isPrime(int n) {
if (n < 2) return false; // 0、1 與負數都不是質數
if (n == 2) return true; // 2 是唯一的偶數質數
if (n % 2 == 0) return false; // 其他偶數直接排除
// 只需檢查到 sqrt(n):若 i*i 已超過 n 就不可能有因數
for (int i = 3; i * i <= n; i += 2) { // 只試奇數
if (n % i == 0) {
return false; // 找到因數,提早結束
}
}
return true;
}
int main() {
int nums[] = {1, 2, 17, 18, 97, 100};
for (int x : nums) {
std::cout << x << (isPrime(x) ? " 是質數\n" : " 不是質數\n");
}
return 0;
}
// 輸出:1 不是質數
// 輸出:2 是質數
// 輸出:17 是質數
// 輸出:18 不是質數
// 輸出:97 是質數
// 輸出:100 不是質數
這支程式同時用上了 if/else 早期返回、for 配合 i += 2 步進、break 概念(用 return 提早結束)、% 取餘、三元運算子與 range-based for。其中 i * i <= n 而非 i <= sqrt(n) 是刻意的:避免重複呼叫浮點 sqrt 與型別轉換,是 C++ 慣用的效能寫法,把迴圈次數從 $O(n)$ 降到約 $O(\sqrt{n})$。
常見錯誤
把這幾條貼在螢幕邊:
=寫成條件:if (x = 0)是賦值不是比較。永遠記得相等是==,並開-Wall讓編譯器幫你抓。switch漏break:少一個break就會貫穿到下一個case。刻意貫穿請用[[fallthrough]];標明。- range-based for 忘了用參考:
for (BigObject x : container)會逐個拷貝。唯讀就用const auto&,要改就用auto&。 - 迴圈條件忘記讓變數變化:
while (n != 1)但迴圈內沒有更新n,就是無窮迴圈。每寫一個while都自問「這個條件最終會變成假嗎?」 - 混用
&&與&:邏輯且是&&(短路),位元且是&(兩邊都算)。在if條件裡幾乎一定要用前者。
深入探討(研究所視角)
寫得出來只是起點。當你的迴圈每秒要跑數十億次時,理解編譯器與硬體如何看待你的流程控制,才能寫出真正快的程式。
分支預測:CPU 在賭你會走哪條路
現代 CPU 採用深度管線(pipeline),會在前一條指令還沒算完時就預先抓取、解碼後續指令。但遇到 if、迴圈條件這類條件分支時,CPU 還不知道結果是真是假,於是它會猜——這就是分支預測(branch prediction)。猜對,管線全速前進;猜錯,已經預取進管線的指令全部作廢、回頭重來,這個「分支誤判懲罰(branch misprediction penalty)」在現代處理器上往往是 10~20 個時脈週期。
這帶來一個反直覺的效能現象:處理「已排序」資料常常比「未排序」快得多,即使演算法完全一樣。考慮這段:
long sum = 0;
for (int i = 0; i < N; ++i) {
if (data[i] >= 128) { // 這個分支好不好預測,決定了速度
sum += data[i];
}
}
若 data 已排序,前半段一律 < 128、後半段一律 >= 128,分支結果有強烈規律,預測器幾乎全猜中;若資料隨機,>= 128 大約一半機率成立,預測器形同擲銅板,誤判率飆高,實測可能慢上數倍。這是著名的 Stack Overflow 經典問題背後的真相。
身為程式設計者,你能做的有幾個層次:
- 讓資料對分支友善:能排序就排序,把可預測的規律餵給硬體。
- 改用無分支(branchless)寫法:把條件轉成算術。例如
sum += data[i] & -(data[i] >= 128),利用bool轉int為 0/1 的特性,用位元遮罩取代if,徹底消滅這個分支。這種技巧在效能關鍵路徑很常見,但會犧牲可讀性,請只在量測過、確認是瓶頸後才用。 - 給編譯器提示:C++20 提供
[[likely]]/[[unlikely]]屬性,告訴編譯器哪條路較常走,影響它的程式碼佈局(把熱路徑放在一起以利指令快取):
if (ptr != nullptr) [[likely]] {
process(ptr);
} else [[unlikely]] {
handleError();
}
這正是 C++ 與 Python 的世界觀差異:Python 的 if 經過位元組碼直譯,分支預測的影響被直譯器開銷淹沒;C++ 的 if 直接對應機器層的條件跳轉,你寫的每個分支都實實在在地與 CPU 管線互動。
迴圈最佳化:編譯器替你重寫程式
當你開 -O2 或 -O3,編譯器(GCC、Clang/LLVM)會對迴圈動一系列手術,常見的有:
- 迴圈展開(loop unrolling):把
for迴圈體複製數份,一次處理多個元素,減少分支判斷與計數更新的次數,也給後續最佳化更多空間。 - 向量化(vectorization):把「對每個元素做相同運算」的迴圈,改用 SIMD 指令(如 SSE、AVX)一次處理 4、8、16 個資料。原本逐元素相加的迴圈,可能被編譯成單一條向量加法指令。
- 迴圈不變量外提(loop-invariant code motion):把迴圈內每次結果都相同的計算搬到迴圈外,只算一次。這也是為什麼前面質數例子寫
i * i <= n而非每輪呼叫sqrt——但即使你寫了sqrt(n),好的編譯器也可能幫你提出去。 - 強度削減(strength reduction):把迴圈內昂貴的乘法換成便宜的加法(例如把
i * stride改成累加)。
關鍵心法是:你的工作是寫出意圖清楚、沒有 undefined behavior 的程式碼,把微觀最佳化交給編譯器。 為什麼「沒有 undefined behavior」如此重要?因為編譯器的許多激進最佳化,正是建立在「程式員保證不會發生未定義行為」這個假設上。例如有號整數溢位在 C++ 是未定義行為,編譯器便可假設 i + 1 > i 永遠成立,從而安全地展開或向量化迴圈;一旦你的程式真的溢位,最佳化過的結果就可能與你預期天差地遠。這是 C++ 「相信程式員」哲學的雙面刃:它換來了極致效能,也要求你對語言規則有更深的敬畏。
想看編譯器到底把你的迴圈變成什麼?把程式碼貼到 Compiler Explorer(godbolt.org),開 -O2,比對組合語言輸出。你會發現一個簡單的累加迴圈,在最佳化後可能完全認不出原貌——這正是理解「零成本抽象」最直接的方式:高階的 range-based for 與手寫指標迴圈,最佳化後往往產生一模一樣的機器碼。抽象是免費的,前提是你信任並善用這套編譯工具鏈。
下次寫迴圈時,不妨多想一層:這個分支好預測嗎?這個迴圈能向量化嗎?我有沒有不小心擋住編譯器的最佳化?把這些問題放進腦中,你就從「會用流程控制」進階到「懂流程控制」了。