Learning Objectives
- 區分機器語言、組合語言與高階語言,說明組譯器、編譯器、直譯器各自的角色
- 認識幾個重要高階語言出現的年代與用途(FORTRAN、COBOL、BASIC、C、Python、Java、JavaScript)
- 說明編譯器的主要階段:詞法分析、語法分析、語意檢查、產生程式碼
- 比較編譯與直譯在執行速度、錯誤發現時機與可攜性上的差異,認識位元組碼、虛擬機器與即時編譯
這一課要回答的問題
第 10 課說過,CPU 只看得懂一串一串的 0 與 1:機器指令。可是你寫的是:
total = price * count
你可能也聽過這些說法:
- 「Python 比 C 慢」
- 「這個軟體只有 Windows 版,Mac 不能用」
- 「Java 寫一次,到處都能執行」
這一課要回答:人寫的程式是怎麼變成 CPU 能執行的指令?「編譯」和「直譯」差在哪裡?
機器語言、組合語言、高階語言
同一件事「total = a + b」,在三個層次長這樣(指令是本課模擬器自訂的教學用指令集):
| 層次 | 寫法 | 誰看得懂 |
|---|---|---|
| 機器語言(machine language) | 20 00 00 20 00 01 30 21 00 02(十六進位) |
CPU 直接執行;每種 CPU 架構的指令編碼都不同 |
| 組合語言(assembly language) | LOAD a LOAD b ADD STORE total |
用好記的英文縮寫代替數字,由組譯器(assembler)一對一翻成機器碼;仍然綁定某一種 CPU |
| 高階語言(high-level language) | total = a + b |
接近人的思考方式,不必管暫存器與記憶體位址;一行可能變成好幾條機器指令,由編譯器或直譯器處理 |
早期的程式設計師直接寫機器碼或組合語言,程式又長又難改,換一台不同的電腦就要重寫。高階語言讓人專心在「要做什麼」, 把「怎麼用這顆 CPU 做到」交給翻譯程式。
幾個重要的高階語言
| 年代 | 語言 | 起源與用途 |
|---|---|---|
| 1957 | FORTRAN | IBM 的 John Backus 團隊開發,第一個廣泛使用的高階語言,用於科學與工程計算 |
| 1959 | COBOL | 由業界與政府組成的委員會(CODASYL)制定,用於商業資料處理;至今仍有銀行與政府系統在使用 |
| 1964 | BASIC | 美國達特茅斯學院(Dartmouth College)開發,讓非理工科的學生也能寫程式 |
| 1972 | C | 貝爾實驗室的 Dennis Ritchie 為了開發 Unix 作業系統而設計;許多作業系統與後來的語言都受它影響 |
| 1991 | Python | Guido van Rossum 在荷蘭的研究機構 CWI 開發,語法簡潔易讀 |
| 1995 | Java | 昇陽電腦(Sun Microsystems)發表,編譯成位元組碼在虛擬機器上執行 |
| 1995 | JavaScript | Brendan Eich 在網景(Netscape)開發,讓網頁可以在瀏覽器裡執行程式 |
語言還可以依程式設計典範(paradigm)分類:一步一步下指令的程序式(C)、以物件組織程式的物件導向(Java)、 以函數組合為主的函數式(Haskell),以及只描述「要什麼結果」的宣告式(SQL)。很多語言(Python、JavaScript)同時支援好幾種寫法。
編譯器怎麼翻譯一支程式
編譯器(compiler)在執行之前,把整支原始碼一次翻譯成目標程式(機器碼或其他指令),主要經過這幾個階段:
| 階段 | 做什麼 | 例子 |
|---|---|---|
| 詞法分析(lexical analysis) | 把一長串字元切成有意義的最小單位:token(關鍵字、名稱、數字、運算子) | while i <= n → while、i、<=、n |
| 語法分析(parsing) | 依照語言的文法,把 token 組成語法樹(syntax tree),運算的先後順序就在樹的形狀裡 | a + b * 2 的樹,* 在下層先算 |
| 語意檢查(semantic analysis) | 檢查語法正確但意思有問題的地方:變數沒有宣告、型別不合 | 把字串和數字相乘 |
| 產生程式碼(code generation) | 把語法樹轉成目標指令,常再經過最佳化(optimization)讓它更快、更小 | while 變成比較指令+條件跳躍+跳回去 |
直譯器(interpreter)則不先翻成一整支目標程式,而是讀一段、分析一段、馬上執行一段。 下面的模擬器用的是本課自己設計的小語言,你可以按每個階段的按鈕,看同一支程式一步一步變成指令;也可以改用直譯器執行,比較兩種做法。
逐步執行:堆疊機器怎麼執行指令
剛剛模擬器產生的指令,是給一台「堆疊機器」執行的。這個 Python 範例就是一台極小的堆疊機器(也就是一個直譯器):它不斷「取一條指令、看是什麼、照著做」。看上方的 stack 怎麼長高又變矮:PUSH 放一個數上去,MUL、ADD 取出兩個數算完再放回去。
想一想:語言的層次與堆疊機器
關於組合語言,下面哪一個敘述正確?
把上面堆疊機器的指令換成下面這兩行,執行後會印出什麼?
ops = ['PUSH', 'PUSH', 'ADD', 'PUSH', 'MUL', 'PRINT']
args = [2, 3, 0, 4, 0, 0]
原始碼 a + b * 2 經過語法分析後,語法樹最上層(根)的運算是?
編譯、直譯,以及兩者之間
| 編譯(compile) | 直譯(interpret) | |
|---|---|---|
| 什麼時候翻譯 | 執行之前,整支程式一次翻完 | 執行的同時,一段一段處理 |
| 執行速度 | 通常比較快:翻譯與最佳化只做一次 | 通常比較慢:執行時還要花時間分析 |
| 錯誤什麼時候發現 | 語法、型別等錯誤在編譯時就發現,程式根本不會開始跑 | 執行到那一行才發現;沒執行到的錯誤可能一直藏著 |
| 修改後 | 要重新編譯 | 改完直接執行,適合邊試邊改 |
| 可攜性 | 編出來的執行檔綁定某種 CPU 與作業系統(所以有 Windows 版、Mac 版) | 有直譯器的地方就能執行同一份原始碼 |
| 例子 | C、C++、Go、Rust | 早期的 BASIC、殼層腳本(shell script) |
現代的語言大多介於兩者之間:
- 位元組碼+虛擬機器:Java 先編譯成和 CPU 無關的位元組碼(bytecode),由各平台的 Java 虛擬機器(JVM)執行,這就是「寫一次,到處執行」。
Python(官方實作 CPython)也會先把原始碼編譯成位元組碼(你可能看過
.pyc檔),再由虛擬機器一條一條執行,所以常被歸類為直譯式語言。 - 即時編譯(just-in-time,JIT):執行時發現某段程式跑得很頻繁,就把它即時編譯成機器碼。Java 虛擬機器與瀏覽器的 JavaScript 引擎都用這種技術,兼顧可攜性與速度。
所以「Python 比 C 慢」的主要原因之一,是 Python 程式執行時還要經過虛擬機器一條一條處理位元組碼; 同一個演算法,用 C 編譯成機器碼直接在 CPU 上跑,通常快很多。但 Python 寫起來快、改起來快,很多時候開發時間比執行時間更重要。
下面的模擬器準備了四支各有一個錯誤的程式。猜猜每個錯誤會在哪一個階段被發現,再實際編譯驗證。
想一想:編譯與直譯
程式裡有一行 x = (3 + 4(少了右括號)。編譯器會在哪一個階段發現?
你在 Windows 上用 C 編譯出 game.exe。把這個檔案複製到一台 Mac,為什麼不能直接執行?
關於 Java 的「寫一次,到處執行」,下面哪一個說明正確?
重點整理
- 機器語言是 CPU 直接執行的二進位指令;組合語言用縮寫代表機器指令,由組譯器一對一翻譯;高階語言接近人的思考,由編譯器或直譯器處理。
- 重要的高階語言:FORTRAN(1957,科學計算)、COBOL(1959,商業)、BASIC(1964,教學)、C(1972,Unix)、Python(1991)、Java 與 JavaScript(1995)。
- 編譯器的階段:詞法分析(切成 token)→ 語法分析(建語法樹)→ 語意檢查 → 產生程式碼(常加上最佳化)。
- 迴圈與 if 在指令層次都是「比較+條件跳躍+跳躍」;堆疊機器靠堆疊存放計算的中間結果。
- 編譯:先翻完再執行,通常比較快、錯誤提早發現,但執行檔綁定平台;直譯:邊分析邊執行,改了就能跑、可攜,但比較慢,沒執行到的錯誤會藏著。
- Java、Python 先編譯成位元組碼再由虛擬機器執行;即時編譯(JIT)在執行時把常用的部分編成機器碼。
想實際寫程式,可以接著上 Python 軌;想看一行程式在 CPU 裡怎麼執行,回頭複習第 10、11 課。
下一課:開始寫程式。變數、型別與控制流程是所有程式語言共同的基本元素。