Skip to content

Stanford CS107 Lecture 2:第一支 C 程式,以及 binary 與 hexadecimal

2026年8月22日 1 分鐘
TL;DR 第二講把 C 放回 Unix 的歷史與開發環境:拆解 header、main、printf、argc/argv,走過 ssh、emacs、make 與執行檔,再用位值系統推導 8 bits = 1 byte、byte 的 256 種 pattern,以及 binary/hexadecimal 的雙向換算。
目錄
  1. 講次資料與材料邊界
  2. 本講完整 agenda
  3. C 不是為了教學而縮小的語言
  4. 第一支 C 程式:每一行都在指定一個契約
  5. printf:格式字串也是型別責任
  6. Boolean 需要 header,真值則來自運算式
  7. argc 與 argv:shell 如何把啟動方式交給程式
  8. 從文字檔到執行檔的日常流程
  9. 從 unexpected behavior 轉進 bits and bytes
  10. 一個 bit 只有兩種狀態,組合後才有表示能力
  11. 位值系統:十進位和二進位其實是同一件事
  12. 十六進位是 binary 的人類可讀壓縮
  13. 一套不靠猜的換算流程
  14. 這一講尚未完成的事
  15. 把兩條線接回來
  16. 延伸閱讀與練習
    1. 最小路線
    2. 標準路線
    3. 深入路線
  17. 參考資料

🌏 English version

CS107 第二講把兩條看似分開的線接起來:用 Unix 工具寫第一支 C 程式,以及用 bit pattern 表示數字。前半段回答「C 程式從文字檔到可執行檔會經過什麼」,後半段回答「程式裡的值到了記憶體究竟是什麼」。兩者合起來,才是這門系統課真正的起點。

這篇依 Winter 2026 Lecture 2 官方投影片 的順序完整走一遍。它不把 C 寫成縮小版 C++,也不把二進位換算寫成孤立算術題。主脊是同一個問題:當高階抽象拿掉一些保護後,程式設計者必須親自掌握哪些層次?

講次資料與材料邊界

本講的錄影、lecture code 與完整終端 demo transcript 沒有公開。投影片只給出從 AFS 複製 lect02 demo 的命令,因此本文不重建那個「unexpected behavior」示範,也不宣稱知道現場執行結果。公開 PDF 足以支持 C 程式骨架、工具流程、位值表示與換算練習;缺的部分會停在缺口,不拿其他學期材料拼接。

本講完整 agenda

  1. C 的歷史:Bell Labs、Unix、B 語言,以及 C 新增的能力。
  2. C、C++、Java 與 Python 的共通點、限制與語言哲學。
  3. 第一支 C 程式:comment、header、mainprintf、return value。
  4. 熟悉的控制語法、stdbool.hbool 與 command-line arguments。
  5. sshemacsmake、執行與清理的日常工作流程。
  6. Bits and Bytes 主題的動機,以及未公開內容的 unexpected-behavior demo。
  7. bit、byte、byte addressability 與位值系統。
  8. 十進位和二進位互轉、乘除 base 的規律。
  9. 十六進位數字、0x0b 前綴,以及 binary/hex 每四位分組換算。
  10. unsigned、signed 與 floating-point representation 的後續入口。

C 不是為了教學而縮小的語言

投影片把 C 的誕生放在 1969 到 1972 年間:Dennis Ritchie 在 Bell Labs 建立它,第一個穩定形態約於 1972 年出現,目的包含實作 Unix。早期 Unix 使用 assembly,難維護且幾乎無法移植;B 語言又缺少強型別、array 與 record。C 從 B 延伸,加入 charlong、pointer、array、record 與 pointer arithmetic,讓程式能明確推理記憶體。

C、C++ 與 Java 共享不少表面元素:primitive data types、算術與邏輯運算子、forswitchif/else 與函式。從 CS106B 過來,很多程式片段看起來熟悉。但 C 沒有 operator overloading、default arguments、真正的 pass by reference 與 object orientation,原生函式庫也少。更關鍵的是,它的 runtime model 極小,預設幾乎不做執行期錯誤檢查。

投影片把 C 稱為 procedural:主要工作是寫函式,而不是先定義 class 再呼叫 method。相較於 Python、C++ 與 Java,C 的 footprint 小、速度快,代價是許多檢查與資源管理責任落到程式設計者身上。它仍值得學,因為 memory、pointer 與 data layout 明確可見,且許多 operating system、compiler、database、driver 與 embedded firmware 都以 C 實作。看過 C 直接暴露的問題,也比較容易理解 Rust、Go 等系統語言為何加入那些安全設計。

第一支 C 程式:每一行都在指定一個契約

投影片的 hello.c 很短:

/*
 * hello.c
 * This program prints a welcome message
 * to the user.
 */
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello, world!\n");
    return 0;
}

最上方是 block comment,C 也支援 // inline comment。註解不參與執行,但它應說明程式目的或不容易從程式碼看出的理由,不必把下一行英文再翻一次。

#include <stdio.h> 讓程式看得到 printf 的宣告。標準函式庫 header 使用角括號;專案自己的 header 則通常使用引號,例如 #include "wordle-utils.h"。這裡常被口語說成「import」,但它和 Python module import 並不是同一套執行期機制。就本講需要掌握的層次,它先提供編譯器檢查呼叫所需的宣告;更完整的 preprocessing、compilation 與 linking 會在系統課後續逐步展開。

main 是程式進入點。回傳型別 int 表示它交回一個小整數狀態碼,0 表示成功。這個 return value 不是印給使用者看的訊息,而是交給啟動程式的環境。Shell 因此能判斷上一個命令是否成功,script 也能據此決定下一步。

printf("Hello, world!\n") 把文字送到標準輸出,\n 是換行。C statement 以分號結束;少一個分號通常是 compile-time error,而不是程式跑到那裡才臨時猜測。

printf:格式字串也是型別責任

投影片將 printf 概括為:

printf(control, arg1, arg2, arg3, ...);

Control string 中的 placeholder 會依序取用後續參數。此處介紹 %s 對 string、%d 對 integer:

char *department = "CS";
int number = 107;
printf("You are in %s%d\n", department, number);

輸出是 You are in CS107。值得現在就養成的習慣,是把 placeholder 數量與參數逐一對齊,並確認型別正確。C 的 minimalist runtime 不會替所有錯配提供友善例外;format string 和實際參數不一致可能產生錯誤甚至未定義行為。即使這一講尚未展開底層原因,也可以先把 compiler warnings 當成必須處理的訊號。

熟悉語法投影片還展示 intdoublecharforifwhile 與 function call。第一行 int x = 23 少了分號,原樣編譯會報錯。不要因為程式碼出現在投影片就關閉編譯器;把片段放進最小程式,以 warnings 開啟的方式驗證。

Boolean 需要 header,真值則來自運算式

若要使用 bool,投影片示範引入 stdbool.h

#include <stdio.h>
#include <stdbool.h>

int main(int argc, char *argv[]) {
    bool x = argc > 2 && argv[argc - 1][0] != 'A';
    if (x) {
        printf("Hello, world!\n");
    } else {
        printf("Greetings, traveler!\n");
    }
    return 0;
}

條件由兩部分組成:參數數量必須大於二,而且最後一個參數的第一個字元不能是 A&& 是 logical AND;只有兩邊條件都成立,x 才為真。這裡的 argv[argc - 1][0] 已經提前露出後面數講會拆解的模型:argv 是字串陣列,先選最後一個字串,再選它的第一個字元。

閱讀這種密集運算式時,可以先把 argc > 2 拆成有名字的 bool,讓存取 argv 的前置條件更明顯。若 argc 太小,就不該碰不存在的額外參數。

argcargv:shell 如何把啟動方式交給程式

標準骨架是:

int main(int argc, char *argv[])

argc 記錄 argument count,argv 保存 argument vector。投影片執行:

./args 1 2 "3 4" five

程式收到五個 argument。第零個是 ./args,接著是 123 4five。引號讓 shell 把中間有空白的 3 4 保留為一個 token,而不是兩個。這個切分發生在程式開始前;C 程式拿到的已是 token 陣列。

投影片的 loop 使用 size_t i,並以 %zu 印出:

for (size_t i = 0; i < argc; i++) {
    printf("Argument %zu: %s\n", i, argv[i]);
}

最直接的練習是自己編譯 args.c,依序測試沒有額外參數、一個參數、帶空白的引號參數,以及空字串 ""。每次先預測 argc 與每個 argv[i],再核對輸出。你會立刻看見 shell parsing 與 C string 是兩個不同層次。

從文字檔到執行檔的日常流程

課程列出的 customary workflow 是:

  1. ssh 遠端登入 Myth。
  2. emacs 編輯 C 原始碼;Ctrl-x Ctrl-s 儲存,Ctrl-x Ctrl-c 離開。
  3. make 按提供的 Makefile 編譯。
  4. ./myprogram 執行產生的 executable,可附 command-line arguments。
  5. make clean 移除 executable 與其他 compiler files。

make 不是 C compiler;它依 Makefile 判斷哪些目標要重建,再呼叫實際編譯工具。看到「make failed」時,要往上找 compiler diagnostic。./myprogram./ 則明確指定目前目錄下的檔案,因為 shell 依 PATH 搜尋命令,不保證搜尋 current directory。編譯成功只代表產生 executable,還不代表它執行正確。

投影片還預告下一週使用 gdb 除錯,以及用 Valgrind 找 memory error 與量測效率。本講沒有教它們的操作,所以此處不提前展開。現在先建立乾淨循環即可:修改、儲存、編譯、讀第一個錯誤、修正、重新編譯、執行、檢查 exit status。

從 unexpected behavior 轉進 bits and bytes

Topic 1 的問題是:電腦如何表示 int 或 floating-point value?投影片給三個動機。理解表示法能解釋 computer arithmetic 的限制,能幫助更有效率地做某些算術,也能更緊密地編碼資料。

課堂安排了一段 unexpected-behavior demo,並列出:

cp -r /afs/ir/class/cs107/lecture-code/lect02 .

這個路徑位於 Stanford AFS,公開讀者沒有足夠材料核對程式與輸出。能確定的是它用來把「C 程式的直覺結果」導向「有限 bit representation 的真實結果」。不能確定的是具體展示哪一種溢位或錯誤,因此本文不替它命名。材料缺口本身也是系統閱讀的一部分:不知道就停在不知道。

一個 bit 只有兩種狀態,組合後才有表示能力

Bit 是 binary digit,單一 bit 可取 01。把多個 bit 組合起來,就像十進位把多個 digit 排成數字,可以表示更多狀態。八個 bits 組成一個 byte,所以一個 byte 有:

2^8 = 256

種 bit pattern。若把它解讀為 unsigned integer,範圍是 0255。最大值可逐項加總 2^7 + ... + 2^0,也可以看成 2^8 - 1

投影片強調 memory 是一個大型 byte array,而且是 byte-addressable:程式不能用一般記憶體位址單獨指出其中一個 bit,只能定位整個 byte。電腦底層依然操作 bits,只是 image、audio、video 與 text 都各自定義如何把 bit patterns 解讀成有意義的資料。

同一串 bits 本身不帶 unsigned integer、字元或像素標籤;型別、指令與檔案格式才決定解讀方式。CS107 後續會反覆在 pattern 不變時切換解讀視角。

位值系統:十進位和二進位其實是同一件事

十進位 5934 可寫成:

5 × 10^3 + 9 × 10^2 + 3 × 10^1 + 4 × 10^0

每個位置使用 base 的次方作為權重,digit 範圍從 0base - 1。二進位完全相同,只是 base 變成二,digit 只剩零與一。例如:

0b1011 = 1 × 2^3 + 0 × 2^2 + 1 × 2^1 + 1 × 2^0 = 11

最左側是 most significant bit(MSB),最右側是 least significant bit(LSB)。名稱描述位置對數值權重的影響,不表示 MSB 永遠有同一個語意;在 signed representation 裡,它的解讀會再改變,那是後續講次的內容。

從 binary 轉 decimal,只要把所有為一的位置權重加起來。0b1010 是八加二,所以是十。從 decimal 轉 binary,則可反覆找不超過目標值的最大二次方:十四先取八,餘六;再取四,餘二;再取二,得到 0b1110

另一個關鍵規律來自位值:在 base 末尾補一個零,相當於乘以 base;去掉末位並做整數除法,相當於除以 base。十進位 7453 × 10 成為 74530,二進位 0b1100 × 0b10 成為 0b11000。不要把它背成二進位特技;它是所有 positional notation 共通的結構。

十六進位是 binary 的人類可讀壓縮

32-bit 或 64-bit 數字若全寫 binary 會很長。Hexadecimal 使用 base 16,digits 是 09 加上 af,後六個依序代表十到十五。因為 16 = 2^4,每一個 hex digit 正好對應四個 bits;兩個 hex digits 正好對應一個 byte。

在 C 與相關工具中,0x 前綴表示 hexadecimal,0b 常用來標示 binary。投影片的例子是:

0xF5 = 0b11110101 = 245

換算 hex 到 binary 時,每一位獨立展開成四 bits。0x173A 可寫成:

1    7    3    A
0001 0111 0011 1010

反方向要從右側開始每四 bits 分組,不足四位就在最左邊補零。投影片將 0b1111001010 分成 0011 1100 1010,得到 0x3CA。從右分組很重要,因為最右邊固定是最低位;若從左側隨意切,位值會錯位。

投影片最後拿同一個 byte 比較三種寫法:decimal 165 對人熟悉,但不容易直接看每個 bit 開關;binary 0b10100101 最透明,卻難讀;hex 0xA5 則保留與 binary 的快速對照,又比完整 bit string 短。Hex 並沒有改變值,它只是選擇更適合系統工作的人類表示方式。

一套不靠猜的換算流程

練習不要只做「看答案覺得合理」。拿一個 byte,例如 0xA5,完成 hex → binary → decimal → binary → hex 的閉環。Binary 轉 decimal 時只加為一位置的權重;binary 轉 hex 時固定從右側四位分組。任何一段回不去,就代表位值或分組仍有漏洞。

這一講尚未完成的事

最後一頁列出三種 number representation:unsigned integers、signed integers 與 floating point。Lecture 2 只完整建立第一個入口,也就是先理解有限 bit pattern 與 unsigned 範圍。負數怎麼編碼、同一組 bits 如何解成 signed value、floating point 為何有精度問題,都還沒在本講說明。

同樣地,本講展示 char * 與 array indexing,卻尚未給出完整記憶體模型。能依範例使用,不代表已經理解 pointer。

把兩條線接回來

C workflow 與 number representation 並不是一堂課硬塞的兩個主題。當你用 make 編譯原始碼,compiler 會把語言層的型別與控制流程轉成 executable;當 executable 執行,變數最後必須落在有限長度的 bit patterns。前半段教你如何讓程式跑起來,後半段開始解釋它跑起來時擁有什麼物理限制。

這也給除錯一個基本分層。Compiler 拒絕程式就先看 syntax、declaration 與 type;啟動失敗先看 shell、path 與 arguments;執行後算錯才往有限表示與 runtime behavior 查。

下一講會繼續走到整數表示,處理 unsigned、signed、溢位與有限寬度。現在先確保能用位值解釋換算,並區分 shell 字串、C 型別與底層 pattern。

延伸閱讀與練習

最小路線

  1. 編譯並執行 hello.c,再用 shell 檢查它的 exit status。
  2. args.c,預測四組不同 command-line invocation 的 argcargv
  3. 手算 0b101001010xA5 與 decimal 165 的閉環換算。

標準路線

  1. printf 故意放入一個錯誤 placeholder,以 warnings 開啟的 compiler 觀察 diagnostic,再修回來。
  2. 用提供的 Makefile 比較第一次 make、未改檔再次 make、修改原始碼後 make 的行為。
  3. 自選五個 byte,為每個值寫出 decimal、binary 與 hexadecimal。

深入路線

  1. man printf%d%s%zu 對應的參數型別,將結果和程式逐項核對。
  2. 在 shell 測試未加引號、有雙引號、空字串與萬用字元的 arguments,記錄 shell 在程式啟動前做了什麼。
  3. 畫一張 source file → preprocessing/compilation/linking → executable → process 的問題定位圖;本講沒提供的細節先標成待後續驗證。

參考資料