JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

超越 C89:C11、C17 與 C23 改變了什麼

你在前面幾階學到的 C 是歷久不衰的核心——但這個語言一直在演進。這裡是 C11、C17 與 C23 究竟加了什麼、以及為何現代方言能讓你的程式更安全、更銳利的地圖。

一個語言,多個標準

你已經會寫能跑的 C。那為什麼要花一整階談「現代 C」?因為你在前面幾階遇到的 C,是刻意挑選的歷久不衰核心:指標、堆疊與堆積、結構、前置處理器。但 C 並沒有凍結。它由一份成文的語言標準所規範,委員會每隔幾年修訂一次,而每次修訂都悄悄加入一些工具,讓同樣的程式更安全、更清楚,或編譯得更快。這份指南就是地圖。其後的四篇會放大細看各個特性;這一篇先給你全局,後面的內容才有地方落腳。

「C89」這個名字來自語言首次標準化的年份:1989 年,由 ANSI 制定(內容完全相同的 ISO 版本標註為 1990 年,所以你也會看到「C90」)。這是大多數教科書教的方言,因為它幾乎到處都能跑。其後出現了 C99(你大概早就在用而不自知——// 註解、在區塊中間宣告變數、以及來自 <stdbool.h> 的 bool 都是從這裡來的),接著就是這一階關心的三個版本:C11、C17 與 C23。

C89 / C90  (1989/1990) ---- the timeless core
C99        (1999)      ---- // , mid-block decls, <stdbool.h>, long long
C11        (2011)      ---- _Generic, _Static_assert, _Alignas, _Noreturn, threads
C17 / C18  (2017/2018) ---- a bug-fix release: no new features, clarifications
C23        (2024)      ---- bool/true/false keywords, nullptr, [[attributes]], typeof, constexpr
C 標準的演進脈絡。C17 刻意沒有新增任何東西;C11 與 C23 帶來這一階要研究的特性。

C11:重頭戲

C11 是現代 C 真正開始的地方,這一階大部分內容都住在這裡。對日常系統程式而言,有四項新增最重要。第一,_Generic:一種在編譯期根據引數的型別來挑選運算式的方法,它終於讓單一巨集能對 int x、double x 或 char *s 表現出不同行為,而不必為每個型別各寫一個函式。這一階的第二篇就專門講它。

第二,_Static_assert:一種由編譯器在編譯時、而非執行時檢查的斷言。寫下 _Static_assert(sizeof(int) == 4, "need 32-bit int"),若它為假,編譯就會帶著你的訊息中止——這個錯誤在程式還沒誕生前就被攔下了。第三,_Alignas 與 _Alignof:你現在可以用可攜的 C 直接陳述與查詢一個型別的記憶體對齊,不必再依賴各家編譯器特有的把戲。這兩者是第三篇的主題。

第四,C11 加入了一個選用的標準執行緒函式庫 <threads.h>,以及位於 <stdatomic.h> 的原子型別——就是你在前面幾階以 POSIX pthreads 形式遇過的同一套並行機制,只是現在受到語言本身的加持。(實務上許多平台仍然把 pthreads 提供得比 <threads.h> 更可靠,所以這一階倚靠你已經會的,而非 C11 的執行緒 API。)C11 也把 _Noreturn 變成關鍵字,這正是 exit() 之類永不返回的函式背後的拼法。

那些底線加大寫的名字

你一定注意到那些難看的拼法:_Generic、_Static_assert、_Alignas、_Noreturn、_Bool。這是有原因的,而且它教給你一件關於標準如何在不破壞整個世界的前提下成長的真實道理。C 標準把所有以底線後接大寫字母開頭的識別字保留給語言自己。一般程式設計師不准這樣命名。所以當委員會需要一個全新的關鍵字時,就能抓一個這類保留名稱來用,而且確定沒有任何既有程式已經把它當變數名用過——藉此保證舊程式碼仍然能編譯。

但 _Static_assert 讀起來很醜。所以標準為其中許多項在標頭檔裡配上一個親切的巨集。引入 <assert.h>,你就能寫 static_assert 取代 _Static_assert;引入 <stdalign.h> 取得 alignas 與 alignof;引入 <stdbool.h> 取得 bool、true、false;引入 <stdnoreturn.h> 取得 noreturn。小寫名稱不過是同一個關鍵字換上更好看的衣服。明白這點,能讓許多「魔法」現出原形:當你在別人的程式裡看到 alignas,你現在知道那就是 C11 的 _Alignas,外加引入了一個標頭檔。

C17 做清理;C23 翻新了表層

C17(也寫作 C18)是最容易概括的標準:它沒有新增任何特性。它是一個維護版本,修正了 C11 措辭中的缺陷與模稜兩可之處。正因為它是個「無事發生」的版本,這點反而值得知道——如果同事說「我們以 C17 為目標」,意思就是「C11,但把規格的瑕疵整平了」。你對 C11 特性所學的一切,到 C17 都不必更動。

C23(2024 年發布)則是表面上的、但令人歡迎的翻新。最醒目的是:幾項原本需要標頭檔的東西,現在成了純粹的關鍵字:bool、true、false 內建了,所以你不再需要引入 <stdbool.h>;static_assert 與那些對齊名稱也都成了關鍵字。C23 還從 C++ 借來三個好點子:一個真正的nullptr 常數用於空指標(比舊的 NULL 或赤裸的 0 更清楚)、用來表示「這個運算式的型別」的typeof 運算子,以及用於真正編譯期常數的 constexpr。第五篇會回頭談這些。

讀起來收益最大的改變是標準屬性:一套統一的、用方括號包起的語法,用來把編譯器無法自行推斷的事實告訴它。標註一個函式,讓編譯器在其回傳值被忽略時發出警告;或標註一個 switch 的 case,讓它不要對一個刻意的貫穿(fall-through)發警告。在 C23 之前,這些只以像 __attribute__((...)) 這樣不可攜的擴充存在。現在它們的拼法每個 C23 編譯器都看得懂。我們在這裡用文字描述屬性名稱;方括號的 C 語法會出現在第五篇的程式範例中。

什麼沒有改變:抽象機器

這裡是最深的一點,也是把這一整階與前面幾階綁在一起的一點。這些標準沒有一個改變了C 抽象機器——語言據以定義的那台想像中的電腦。C 並不承諾做你真實 CPU 所做的事;它承諾的是表現得「彷彿」你的程式跑在這台理想化的機器上。在as-if 規則下,只要這台抽象機器的可觀察行為被保留,編譯器就能自由地重排、刪除或轉換你的程式碼。從 C89 到 C23,這份合約都沒有變。

為什麼這對一個談新特性的階段重要?因為抽象機器也正是未定義行為的居所,而現代 C 給你更好的工具去待在它的規則之內,而不是去削弱那些規則。回想前面幾階那個誠實的定義:未定義行為並不是「編譯器高興怎樣就怎樣」。它是優化器拿走的一張許可證——它獲准假設 UB 從不發生。這正是為什麼一個臭蟲在 gcc -O0 下可以隱形、卻在 gcc -O2 下破壞記憶體:優化器在推理各條程式路徑時,假設你從不觸發 UB。沒有任何新標準能讓 UB 變安全;C11 的 _Static_assert 與 C23 的屬性所做的,是幫你預防那些通向 UB 的情境。