Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

流程控制

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++)跟著敲。

C++ 流程控制概念示意圖

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,只有 truefalse 兩個值。相較於 Python 用「真假值(truthiness)」把空字串、0None 一律當成假,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 執行,直到遇到 breakswitch 結束。貫穿偶爾是刻意設計(多個 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 只能用在整數型(含 charenum),不能直接 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})$。

常見錯誤

把這幾條貼在螢幕邊:

  1. = 寫成條件if (x = 0) 是賦值不是比較。永遠記得相等是 ==,並開 -Wall 讓編譯器幫你抓。
  2. switchbreak:少一個 break 就會貫穿到下一個 case。刻意貫穿請用 [[fallthrough]]; 標明。
  3. range-based for 忘了用參考for (BigObject x : container) 會逐個拷貝。唯讀就用 const auto&,要改就用 auto&
  4. 迴圈條件忘記讓變數變化while (n != 1) 但迴圈內沒有更新 n,就是無窮迴圈。每寫一個 while 都自問「這個條件最終會變成假嗎?」
  5. 混用 &&&:邏輯且是 &&(短路),位元且是 &(兩邊都算)。在 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),利用 boolint 為 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 與手寫指標迴圈,最佳化後往往產生一模一樣的機器碼。抽象是免費的,前提是你信任並善用這套編譯工具鏈。

下次寫迴圈時,不妨多想一層:這個分支好預測嗎?這個迴圈能向量化嗎?我有沒有不小心擋住編譯器的最佳化?把這些問題放進腦中,你就從「會用流程控制」進階到「懂流程控制」了。

AI 共讀助教正在陪你讀:C++ 流程控制:讓程式自己決定走哪條路
嗨!我是這篇文章的共讀助教,只根據〈C++ 流程控制:讓程式自己決定走哪條路〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。