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; }