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

控制流程

控制流程

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