跳轉到

1972 C 語言誕生:Ritchie 為重寫 Unix 而生的系統程式語言#

案發現場#

1960 年代末的貝爾實驗室,Dennis Ritchie 與 Ken Thompson 面臨一個尷尬的處境:他們參與的 Multics 作業系統計畫因過度龐大而被迫中止,實驗室甚至把唯一的大型機 GE-645 撤走了。Thompson 不甘心,在一台報廢的 PDP-7 上用組合語言寫出了一個小型作業系統——Unix 的前身。但組合語言有一個致命問題:它綁死在特定機器上。當實驗室買了新的 PDP-11,整個系統幾乎要從零重寫。

未解之謎是:能不能用一種高階語言寫作業系統,同時保留組合語言的效能與底層控制能力?

這個問題為何重要?當時學界的共識是:作業系統這種「碰硬體」的東西只能用組合語言寫。高階語言(如 FORTRAN、ALGOL)的執行期支援(垃圾回收、邊界檢查、動態記憶體)太重,會拖垮系統。如果有人能證明高階語言也能寫 OS,整個軟體產業的可攜性問題就迎刃而解。

Thompson 先嘗試了 Martin Richards 的 BCPL,將其簡化成自己的 B 語言。但 B 有個實作上的缺陷:它是無型別的,所有資料都是機器字(word),在 PDP-11 這種有 byte 定址、浮點數、多種整數長度的機器上,B 編出來的程式又慢又笨拙。Ritchie 於 1971–1972 年間著手「NB」(New B)計畫,最終演變成 C 語言。

偵查過程#

Ritchie 的核心思路可以概括為一條推理鏈:

  1. BCPL → B 的簡化是對的:語言要小,編譯器才容易移植。
  2. B 的「無型別」錯了:硬體有 byte 與多種字長,語言必須反映這一點。
  3. 但型別不能像 ALGOL 那樣重:不需要執行期型別檢查,型別只是「給編譯器看的佈局描述」。

於是 C 的型別系統誕生了:char、int、float 等基本型別對應硬體的儲存單位,而陣列與結構只是記憶體佈局的語法糖。最關鍵的發明是指標。

B 語言中陣列與指標的概念混在一起,Ritchie 將其系統化。他的關鍵推導是:陣列名在運算式中會「衰變」(decay)為指向首元素的指標,因此:

int a[10];
a[i]  ≡  *(a + i)  ≡  *(i + a)  ≡  i[a]

這個等價關係不是戲法,而是設計的必然:位址算術 \(addr + i \times sizeof(T)\) 由編譯器根據型別 \(T\) 自動計算,程式設計師只需寫抽象的索引。指標讓 C 能直接操作硬體位址(記憶體映射暫存器),又保持語言層次的抽象。

第二個關鍵決策是手動記憶體管理:C 不做垃圾回收,記憶體配置(malloc)與釋放(free)交給程式設計師。這換來了零執行期負擔與可預測的效能,代價是記憶體洩漏與懸垂指標——這筆帳程式設計師們還了五十年。

第三個關鍵是可攜性策略:「一個編譯器搞定所有機器」。Ritchie 將編譯器設計成兩層:前端做語言解析,後端只產生簡單的中間碼,機器相關的部分集中在極少數檔案中。移植 Unix 到新機器時,只需重寫後端。這個「自舉」(bootstrapping)策略配合 Unix 本身,使兩者一起橫掃學術界與工業界。

到了 1978 年,Ritchie 與 Kernighan 出版《The C Programming Language》(俗稱 K&R C),成為事實標準。1983 年 ANSI 成立委員會,1989 年推出 ANSI C(C89),補上了函數原型、void 等缺口,C 正式成為國際標準。

結案報告#

C 語言的遺產無與倫比:

  • 系統程式語言霸權:Unix、Linux、Windows 核心大部分、資料庫(PostgreSQL、MySQL)、嵌入式系統,至今幾乎都以 C 為基礎。
  • 語言系譜:C++(1983-Cplusplus.md)、Objective-C、Java(1995-Java.md)、C#、JavaScript(1995-JavaScript.md)的語法都是 C 家族。連 Python(1991-Python.md)的解譯器 CPython 本身就是用 C 寫的。
  • 程式設計文化:「指標與記憶體」成為資工教育的必修課,「 segmentation fault 」成為一代人的集體記憶。
  • POSIX 標準:C 標準函式庫與 Unix 介面共同定義了「可攜作業系統」的概念。

Ritchie 於 1983 年與 Thompson 共同獲得圖靈獎。C 證明了一件事:抽象化不必付出效能代價——只要你願意讓程式設計師自己承擔責任。

證據與工具#

C 語言示範——指標與手動記憶體:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *name;
    int  age;
} Person;

Person *person_new(const char *name, int age) {
    Person *p = malloc(sizeof(Person));
    p->name = strdup(name);   /* 手動配置 */
    p->age  = age;
    return p;
}

void person_free(Person *p) {
    free(p->name);            /* 手動釋放 */
    free(p);
}

int main(void) {
    Person *p = person_new("Ritchie", 35);
    printf("%s is %d\n", p->name, p->age);
    person_free(p);
    return 0;
}

用 Python 模擬 C 的指標位址算術與 malloc/free 機制:

class FakeHeap:
    def __init__(self, size=256):
        self.mem = bytearray(size)
        self.next_addr = 0
        self.blocks = {}

    def malloc(self, nbytes):
        addr = self.next_addr
        self.next_addr += nbytes
        self.blocks[addr] = nbytes
        return addr

    def free(self, addr):
        self.blocks.pop(addr, None)

    def store_int(self, addr, value, size=4):
        self.mem[addr:addr+size] = value.to_bytes(size, "little")

    def load_int(self, addr, size=4):
        return int.from_bytes(self.mem[addr:addr+size], "little")

heap = FakeHeap()
base = heap.malloc(40)          # 相當於 int a[10]
heap.store_int(base + 2 * 4, 42)   # a[2] = 42,即 *(base + 2)
print(heap.load_int(base + 2 * 4)) # 42,示範 addr + i*sizeof(T)