跳轉到

2024:C23——記憶體安全時代的最新標準#

事件#

2024 年,ISO 正式發佈 ISO/IEC 9899:2024(C23,草案期俗稱 C2x)。歷經多年,C 語言吸收了 C++ 與新語言的經驗,完成自 C11 以來最大的一次實用性更新。

為何重要#

  • 新語法一覽:bool/true/false 成為關鍵字、nullptr、typeof、constexpr、enum 底層型別指定、二進位字面值 0b、數字分隔符 '(1'000'000)、auto 型別推論、空初始化 {} 保證零值、#embed 嵌入二進位檔、char8_t、增強的 Unicode 支援、移除 K&R 舊式函式定義、屬性語法 [[nodiscard]]、檢查到的整數運算 <stdckdint.h>。

理論與實用原因(各新語法為何加入)#

  • bool/true/false 關鍵字化:
  • 彌補的缺陷:C99 的 _Bool 加 <stdbool.h> 巨集是補丁式設計,名稱曖昧(巨集可被#undef);C 用 int 當布林值更是錯誤源頭(if (x = 5) 誤寫)。
  • 理論原因:布林是邏輯的基本型別,語言應有一等公民地位。
  • nullptr:
  • 彌補的缺陷:NULL 巨集本質是 ((void*)0) 或 0,整數與指標混淆(f(0) 傳給指標參數不會報錯)。
  • 實用原因:吸收 C++11 的經驗,型別專屬的空指標讓重載與檢查更精確。
  • typeof:
  • 彌補的缺陷:巨集(如 Linux 的 container_of、min/max)需要重複寫出型別,容易出錯。
  • 實用原因:GCC 擴充 typeof 已被 Linux 核心用了三十年,C23 將既有實踐標準化——「標準化既有實踐」是 C23 的主軸。
  • constexpr:
  • 彌補的缺陷:#define 常數無型別、無作用域;C11 的 _Static_assert 只能斷言不能定義變數。
  • 實用原因:編譯期常數可用於陣列大小、靜態初始化,兼顧型別安全與零成本。
  • #embed:
  • 彌補的缺陷:把二進位檔(字型、韌體、圖片)嵌入 C 程式,只能用 xxd 轉成十萬行位元組陣列,編譯慢到崩潰。
  • 實用原因:嵌入式與韌體開發的普遍需求,一行搞定。
  • 空初始化 {} 零值保證:
  • 彌補的缺陷:未初始化變數是 C 最著名的資安漏洞來源(CVE 常客)。
  • 實用原因:struct s x = {}; 保證全部歸零,比 memset 更安全易讀。
  • 移除 K&R 舊式函式定義:
  • 彌補的缺陷:C89 的舊式宣告不檢查參數,服役 35 年後終於除役——語言史上的「還債」。
  • <stdckdint.h> 檢查到的整數運算:
  • 彌補的缺陷:整數溢位是 C 的半世紀老缺陷與重大資安問題(無符號環繞、有符號 UB)。
  • 實用原因:ckd_add(&r, a, b) 讓加減乘的溢位可被偵測,回應美國 NSA 記憶體安全報告的壓力。

彌補了什麼缺陷#

C23 彌補了 C「布林與空指標是補丁、巨集取代不了語言特性、未初始化與整數溢位兩大老缺陷、K&R 遺毒」的問題;同時貫徹「標準化既有實踐」(typeof、#embed)而非發明新理論的務實路線。在 Rust 崛起、各國政府推動記憶體安全的壓力下,C23 是 C 語言對時代的回應。

程式範例#

bool/true/false 關鍵字化——不再需要 <stdbool.h> 補丁:

/* C99:補丁式設計 */
#include <stdbool.h>     /* 巨集可以 #undef,名稱曖昧 */
bool ok = true;

/* C23:布林是一等公民 */
bool ok = true;          /* 直接用,無需任何標頭檔 */
if (ok) { ... }

nullptr——整數與指標終於分家:

/* C 的缺陷:NULL 本質是 0 或 ((void*)0),與整數混淆 */
int *p = NULL;
f(0);                    /* 想傳空指標?傳了整數,不會報錯 */

/* C23:型別專屬空指標 */
int *p = nullptr;
f(nullptr);              /* 語意明確:這是指標 */

typeof——Linux 核心用了三十年的擴充,C23 標準化:

/* C 的缺陷:巨集需要重複寫出型別,容易出錯 */
#define SWAP(a, b) { typeof(a) t = a; a = b; b = t; }   /* C23 */

int x = 1; double y = 2.5;
SWAP(x, y);              /* 巨集自動依型別宣告暫存變數 */
/* typeof 讓 max/min/container_of 等巨集不再重複型別 */

constexpr——有型別、有作用域的編譯期常數:

/* C 的缺陷:#define 無型別無作用域 */
#define SIZE 100         /* 全域且無型別 */

/* C23: */
constexpr int size = 100;              /* 有型別(int)、有作用域 */
constexpr long long big = 1LL << 40;   /* 編譯期求值 */

int buf[size];                          /* 可用於陣列大小 */
_Static_assert(size == 100, "編譯期驗證");

空初始化 {}——未初始化變數(CVE 常客)的剋星:

/* C 的著名缺陷:未初始化變數內容是垃圾 */
struct config { char name[8]; int port; };
struct config c;         /* c.port 是隨機值!資安漏洞常客 */

/* C99 的補法:要先 memset */
struct config c = {0};   /* C99:只有第一個成員明確歸零,其餘「依規則」 */

/* C23:{} 保證全部歸零,比 memset 更安全易讀 */
struct config c = {};    /* 所有成員保證為零值 */

#embed——二進位檔一行嵌入:

/* C 的缺陷:嵌入二進位檔只能轉成十萬行位元組陣列,編譯慢到崩潰 */
static const unsigned char font[] = {
    0x89, 0x50, 0x4E, 0x47, /* ... xxd 轉出的數萬行 ... */
};

/* C23:一行搞定 */
static const unsigned char font[] = {
#embed "logo.png"        /* 嵌入整個二進位檔,編譯器直接讀 */
};

<stdckdint.h>——半世紀老缺陷「整數溢位」終於可偵測:

#include <stdckdint.h>

/* C 的著名缺陷:溢位無聲無息 */
long long a = 4000000000LL, b = 4000000000LL;
long long sum = a + b;        /* 有符號溢位:未定義行為! */

/* C23:溢位可被檢查 */
long long sum;
if (ckd_add(&sum, a, b))      /* 回傳真表示溢位 */
    fprintf(stderr, "overflow!\n");
else
    printf("%lld\n", sum);

移除 K&R 舊式函式定義——服役 35 年後除役:

/* C23 之前(K&R 舊式,不檢查參數): */
int add(a, b) int a, b; { return a + b; }

/* C23:此寫法已從標準移除,必須寫原型 */
int add(int a, int b) { return a + b; }

相關條目#