Home
探索 Uedu
學生控制台
註冊會員/登入
研究知情同意中心
教師控制台
課程設定
支援與訊息
Uptime 數據

UeduGPTs

--

Jupyters

4

UG26 CISOSE26
臺北 AQI 46 · 臺中 AQI 26 · 臺南 AQI 21 · 高雄 AQI 33

AI 回覆桌面通知

AI 助教回覆完成時顯示桌面通知

聊天訊息通知

同學在討論區發送訊息時通知

聲音通知

每當有新通知時播放提示音

控制流程

控制流程

從食譜到 CPU 管線:循序、選擇、重複三種積木,如何在邏輯正確與機器效率之間取得平衡

為什麼食譜不只是「材料清單」

想像你照著食譜煮一鍋咖哩。如果食譜只列出「洋蔥、咖哩塊、馬鈴薯、水」,你會一頭霧水:要先做什麼?水滾了沒?菜軟了嗎?真正有用的食譜會這樣寫:「先把油熱了;如果洋蔥變透明,就下肉;重複翻炒到表面變色;接著加水煮到馬鈴薯能用筷子戳穿為止。」

你發現了嗎?食譜的價值不在「有哪些材料」,而在「以什麼順序、在什麼條件下、做幾次」。程式也是一樣。程式碼裡的每一行指令就像材料,而真正讓程式「活起來」、能依情況做出不同反應的,是控制流程(control flow)——它決定電腦執行指令的順序、分岔與重複。掌握控制流程,等於拿到了讓電腦「照你的意思行動」的方向盤。

程式設計概念示意圖

三大基本結構:循序、選擇、重複

控制流程聽起來複雜,但其實所有程式的流程都可以拆解成三種基本積木:

  • 循序(sequence):由上往下,一行接一行執行。這是預設行為,最直覺。
  • 選擇(selection):依條件決定要走哪一條路,也就是分支(branch)。
  • 重複(repetition):在某條件成立時,反覆執行同一段程式,也就是迴圈(loop)。

這三種結構看似簡單,卻足以表達任何可被計算的演算法。這不是隨口說說,而是一條有名字、有證明的定理,我們在文末的研究所視角會回頭談它。先讓我們把每一塊積木拿在手上看清楚。

選擇:讓程式學會「看情況」

選擇結構的核心是 ifelse。它讓程式根據一個布林(boolean)條件——也就是只會是「真(True)」或「假(False)」的判斷——決定要不要執行某段程式。

score = 72

if score >= 60:
    print("及格")
else:
    print("不及格")

當條件不只兩種可能時,可以串接多個判斷。Python 用 elif,許多語言用 else if

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 60:
    grade = "C"
else:
    grade = "F"

這裡有個重要觀念:條件是由上往下逐一檢查的,一旦某個條件成立,後面的 elif 就不再檢查。所以條件的順序會影響結果。如果你把 score >= 60 放在最前面,那麼 95 分也只會拿到 C,因為它「先撞上」了 60 分這道門。

重複:把重複的勞動交給電腦

電腦最擅長的就是不厭其煩地做同一件事。重複結構分成兩大類:

  • while 迴圈:當條件成立時持續執行,適合「不知道要做幾次、只知道何時該停」的情境。
  • for 迴圈:走訪一個範圍或一組資料,適合「知道要做幾次」的情境。
# while:印出小於 100 的 2 的次方
n = 1
while n < 100:
    print(n)
    n = n * 2

# for:計算 1 到 10 的總和
total = 0
for i in range(1, 11):
    total = total + i
print(total)  # 55

while 迴圈時最常見的陷阱是無窮迴圈(infinite loop):如果條件永遠為真,程式就會卡死。上面的 while 範例之所以會停,是因為每一輪都讓 n 變大,終究會超過 100。請務必確認迴圈內有「朝向終止條件前進」的動作。

兩個常用的流程控制關鍵字也值得認識:break 會立刻跳出整個迴圈,continue 則跳過本輪剩下的程式、直接進入下一輪。

巢狀與短路:細節裡的魔鬼

巢狀(nesting)

結構可以互相包覆。迴圈裡放迴圈、判斷裡放判斷,就形成巢狀。例如印出九九乘法表,就是一個迴圈套另一個迴圈:

for i in range(1, 10):
    for j in range(1, 10):
        print(f"{i}×{j}={i*j}", end="\t")
    print()  # 每一列結束換行

巢狀很強大,但層數一多就難讀。一般建議巢狀不要超過三層;過深時,可以把內層邏輯抽成函式(function),讓程式回到「淺而清楚」的狀態。

短路求值(short-circuit evaluation)

當條件用 andor 串接多個判斷時,許多語言採用短路求值:只要結果已經確定,就不再評估後面的條件。

# 若 user 是 None,user.is_admin 會出錯。
# 但短路求值讓左邊先擋下來:
if user is not None and user.is_admin:
    grant_access()

and 中,只要左邊為假,整個結果必為假,右邊就被「短路」跳過;在 or 中,只要左邊為真,右邊也被跳過。這不只是效能優化,更是一種保護:上面的例子若沒有短路,當 userNone 時讀取 user.is_admin 就會崩潰。善用短路,能寫出既安全又簡潔的條件。

動手看一個例子:判斷閏年

讓我們用一個經典問題,把選擇與巢狀條件整合起來:判斷某一年是不是閏年(leap year)

閏年規則是:

  1. 能被 4 整除的年份是閏年,
  2. 能被 100 整除的不是閏年,
  3. 除非它同時能被 400 整除,那又是閏年。

我們先用逐步推理走一遍 2000 年與 1900 年:

年份 能被 4 整除? 能被 100 整除? 能被 400 整除? 結論
2000 閏年
1900 平年
2024 閏年
2023 平年

把規則翻成程式,有兩種寫法。第一種用巢狀 if,貼近上面的「先判斷、再例外」的思路:

def is_leap(year):
    if year % 4 == 0:
        if year % 100 == 0:
            if year % 400 == 0:
                return True   # 能被 400 整除,是閏年
            else:
                return False  # 能被 100 但不能被 400,不是
        else:
            return True       # 能被 4 但不能被 100,是
    else:
        return False          # 不能被 4,不是

第二種用布林運算把三層條件壓成一行,配合短路求值,可讀性反而更高:

def is_leap(year):
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

兩種寫法邏輯完全相同,但後者展示了一個重要觀念:清楚的條件運算式往往勝過深層巢狀。能用一個表達意圖的布林式說清楚的事,就不必堆疊多層 if

重點回顧

  • 控制流程決定指令的執行順序、分岔與重複,是程式從「材料清單」變成「會做事的食譜」的關鍵。
  • 三大基本結構是循序、選擇、重複;任何演算法都能由它們組合而成。
  • ifelifelse 的條件由上往下逐一檢查,順序會影響結果;while 適合「不知次數」、for 適合「已知次數」。
  • 巢狀讓結構可以互相包覆但層數宜淺;短路求值andor 在結果確定時停止評估,兼具效能與安全。
  • 善用清楚的布林運算式,常能取代深層巢狀,寫出更易讀的程式。

深入探討(研究所視角)

結構化程式定理:為什麼三種積木就夠了

控制流程的三大結構並非工程慣例,而有堅實的理論根基。Böhm–Jacopini 定理(又稱結構化程式定理,1966)證明:任何可計算的函式,都可以僅用循序、選擇、重複三種控制結構組合的流程圖來表達——不需要任意跳躍。這條定理是 1970 年代「結構化程式設計(structured programming)」運動的理論支柱,也是我們今天能安心宣稱「不用 goto 也能寫出任何程式」的依據。

這也解釋了為何要避免 gotogoto 允許程式從任意一點跳到另一任意一點,會編織出 Dijkstra 在著名文章〈Go To Statement Considered Harmful〉(1968) 中批評的「義大利麵程式(spaghetti code)」:控制流糾纏難解,程式的靜態文字與動態執行軌跡嚴重脫節,使人難以推理其正確性。結構化程式的價值在於,每個區塊都有單一入口、單一出口,讓「閱讀順序」貼近「執行順序」,大幅降低理解與驗證的認知負擔。值得補充的是,現代語言保留的 breakcontinue、提早 return、以及例外處理(exception),可視為受嚴格約束的結構化跳躍——它們在不破壞單一入口的前提下,提供有限而可預測的「出口」,這正是純粹理論與工程實務之間的折衷。

分支預測:抽象之下的硬體真相

到目前為止我們把控制流程當成純粹的邏輯,但在 CPU 內部,分支其實有實打實的效能代價。現代處理器採用深度管線(pipeline):一條指令的取得、解碼、執行被切成多個階段,多條指令在不同階段同時推進,藉此提高吞吐量。問題是,遇到一個 if,CPU 在條件算完之前,並不知道下一步該抓哪段指令。

為了不讓管線停擺,CPU 使用分支預測器(branch predictor):根據歷史紀錄「猜」分支會往哪走,並先把猜中的路徑灌進管線。猜對了,幾乎沒有額外成本;猜錯了,已經進入管線的指令必須全部清空、從正確位置重抓,這個分支誤判懲罰(branch misprediction penalty) 在現代 CPU 上往往達到十幾到二十多個時脈週期。

這帶來一個對研究生很重要的洞見:分支的「可預測性」會直接影響效能。考慮對一個陣列做條件累加:

# 若資料「已排序」,分支高度規律,預測器幾乎全中
# 若資料「隨機」,分支忽真忽假,預測器頻頻誤判
total = 0
for x in data:
    if x > threshold:
        total += x

在編譯式語言(如 C/C++)中,同樣這段程式,對已排序資料的執行速度可能比對隨機資料快上數倍——程式碼一字未改,差別純粹來自分支是否容易被預測。這也是為什麼高效能程式設計會出現「無分支程式設計(branchless programming)」技巧:用位元運算或條件搬移指令(conditional move)取代 if,把資料相依的分支轉成資料相依的算術,藉此完全繞過預測器。

我們可以把分支的期望成本粗略寫成:

$$ C_{\text{branch}} \approx p_{\text{miss}} \cdot P_{\text{penalty}} $$

其中 $p_{\text{miss}}$ 是誤判機率,$P_{\text{penalty}}$ 是每次誤判的週期懲罰。當 $p_{\text{miss}} \to 0$(分支高度規律),分支幾乎免費;當 $p_{\text{miss}} \to 0.5$(純隨機,最難猜),成本達到最大。這也呼應了演算法複雜度分析的一個盲點:兩段同為 $O(n)$ 的程式,實際執行時間可能因為快取行為與分支可預測性而相差數倍。漸近複雜度告訴你「規模如何增長」,但常數因子裡藏著硬體的真相。

把兩端連起來

從 Böhm–Jacopini 的理論到分支預測的硬體,控制流程橫跨了計算機科學的兩個極端。上層,它是讓我們能形式化推理程式正確性的抽象結構——這條路通往程式驗證、靜態分析與型別理論。下層,它是 CPU 必須實際押注、可能押錯的物理事件——這條路通往計算機結構、編譯器最佳化與效能工程。一個 if,同時活在這兩個世界裡。理解控制流程,不只是學會寫 ifwhile,更是學會在「邏輯正確」與「機器效率」之間,看見那條始終存在的張力。

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