C 语言的标准演进以"慢"著称:C11 之后,ISO 在 2018 年发布了 C17(ISO/IEC 9899:2018),这几乎是一个"零新特性"的版本;而 2024 年正式发布的 C23(ISO/IEC 9899:2024)则完全不同——这是自 C11 以来 13 年间最大的一次更新,一口气加入了 nullptr、typeof、constexpr、二进制字面量、属性语法、#embed 等一大批特性。本文先花一小节说清 C17,再用主要篇幅带你认识 C23。

1. C17:一次"纯修复"的发布

C17 没有增加任何语言特性,全部工作是修复 C11 发布后发现的缺陷(DR,Defect Report),并澄清一些有歧义的表述,比如 _Alignof 作用于表达式时的行为、原子类型相关的边界情况。它的价值在于:编译器厂商终于有一个"稳定靶子"可以对齐,__STDC_VERSION__ 变为 201710L。对普通开发者影响几乎为零:用 -std=c17 编译旧代码,行为与 C11 基本一致,可以无痛切换。可以说,C17 的存在意义更多是流程性的——它证明标准维护机制在持续运转。

2. C23 概览:十三年来最大更新

C23 从 2020 年开始起草,2024 年正式发布。设计目标是"减少样板代码、增强类型安全、拥抱现代硬件"。最具代表性的变化:bool / true / false 从"头文件宏"升级为语言关键字;新增 typeof 类型推导与 nullptr 空指针常量;constexpr 允许声明编译期常量对象;支持二进制字面量与数字分隔符;属性语法 [[...]] 从各家编译器的扩展正式标准化;预处理器新增 #embed 与 #elifdef 等。另外,C23 还统一了函数声明的语义:不带参数的 f() 明确表示"不接受参数",古老的 K&R 风格无参声明被移除,这让新代码的行为更可预期。下面挑重点逐个看。

3. 更顺手的类型系统:bool、typeof 与 nullptr

在 C23 之前,用 bool 必须 #include <stdbool.h>,这是 C 语言被吐槽最多的地方之一;C23 直接把它变成关键字,连 include 都省了,旧代码包含头文件也不会出错。typeof 可以推导表达式类型,写泛型宏、声明"与某个变量同类型"的变量都很方便,补齐了 C 在类型推导上的长期空白,也让 #define 里那些容易出错的类型转换有了更安全的替代。nullptr 是类型为 nullptr_t 的空指针常量:类型明确,在 _Generic 分派等需要区分"整数 0"和"空指针"的场景下更精确,语义也更清晰。旧代码完全不用改,NULL 依然合法。

// C23 之前:bool 依赖头文件
#include <stdbool.h>
bool old_way = true;

// C23:bool / true / false 是关键字,无需头文件
bool ok = true;
bool failed = false;

// typeof:推导类型
typeof(3 + 4) x = 7; // int x
typeof(x) arr[4];    // int arr[4]

// nullptr:明确的空指针常量
int *p = nullptr;

4. constexpr 与检查整数运算

constexpr 声明的对象是编译期常量,可用于数组长度、case 标签、位域宽度等需要常量表达式的地方,替代 #define 的一部分工作,而且有类型、有作用域,比宏更安全,调试器里也能直接看到它的值。C23 还新增了 <stdckdint.h>,提供 ckd_add、ckd_sub、ckd_mul 三个"检查溢出"的函数:不溢出时返回 0 并写入结果,溢出时返回 1,结果保持截断后的值。处理用户输入、协议解析时,这一组函数能帮你避免大量未定义行为,比手写"先比较再相加"的防御代码可靠得多。

// constexpr:编译期常量对象
constexpr int SIZE = 1024;
int buffer[SIZE]; // 普通数组,不是变长数组

// 检查整数溢出
#include <stdckdint.h>
int a = 2000000000;
int b = 2000000000;
int sum;
if (ckd_add(&sum, a, b)) {
    puts("溢出!"); // 会走到这里
}

5. 更现代的字面量与属性

二进制字面量 0b1010 和数字分隔符 1'000'000 让常量写起来更接近直觉,位运算代码的可读性提升尤其明显。属性语法把 [[nodiscard]]、[[deprecated]]、[[fallthrough]]、[[maybe_unused]] 等统一成标准形式,替代了各家编译器 __attribute__ 的混乱局面,代码在不同编译器间迁移时不用再写一堆条件宏。其中 [[nodiscard]] 特别实用:函数的返回值被丢弃时编译器会给出警告,非常适合强制检查错误码——返回值被忽略正是 C 程序最常见的 bug 来源之一。

#include <stdio.h>

// 二进制字面量 + 数字分隔符
int mask = 0b1010_0101; // 165
long budget = 1'000'000;

// 属性:返回值不可丢弃
[[nodiscard]] int open_file(const char *path) {
    return path[0] != '\0' ? 0 : -1;
}

int main(void) {
    int fd = open_file("data.txt");
    if (fd < 0) {
        puts("打开失败");
    }
    return 0;
}

6. 预处理增强:#embed 与 #elifdef

#embed 可以把二进制文件的内容在编译期直接嵌入源码:把图片、固件、证书变成字节数组,彻底替代 xxd -i 这类外部工具生成的丑陋 C 文件,还能自动处理对齐与大小限制。注意它需要 GCC 15+ 或 Clang 19+ 的支持,属于 C23 里最"前沿"的特性,旧编译器会直接报错。另外 #elifdef / #elifndef 补上了条件编译的最后一块拼图,以前 #elif defined(X) 的写法可以简化了。

// #embed:把二进制文件内容直接嵌入
// 需要 GCC 15+ 或 Clang 19+
const unsigned char icon[] = {
    #embed "icon.bin"
};

// 更简洁的条件编译
#ifdef DEBUG
    #define LOG(...) printf(__VA_ARGS__)
#elifdef RELEASE
    #define LOG(...) ((void)0)
#endif

7. 如何在你的环境里启用

GCC 从 13 开始支持大部分 C23 特性(选项为 -std=c2x),GCC 14 进一步补全并接受 -std=c23 作为选项名;Clang 16+ 支持大部分特性(-std=c2x);MSVC 也在逐步跟进。细分到具体特性:bool、typeof、nullptr 在 GCC 13 就可用,stdckdint.h 与二进制字面量中的数字分隔符要 GCC 14+,#embed 要 GCC 15+。判断编译器支持程度最快的方法:看预处理宏 __STDC_VERSION__ 是否为 202311L。需要提醒的是,"编译器接受 -std=c23"不等于"所有 C23 特性都可用",#embed、_BitInt 这类特性仍依赖很新的编译器版本;如果你的工具链还是老 GCC(比如嵌入式厂商提供的 4.x/8.x),老老实实写 C11 甚至 C99 更稳妥,别让新语法成为团队协作的障碍。

/* 查看编译器宣称支持的标准版本 */
/* echo | gcc -std=c23 -dM -E - | grep __STDC_VERSION__ */
/* 输出 __STDC_VERSION__ 202311L 表示支持 C23 */
特性C11C17C23
__STDC_VERSION__201112L201710L202311L
bool / true / false头文件提供同 C11语言关键字
typeof
nullptr
constexpr(对象)
二进制字面量 / 数字分隔符
属性 [[...]]无(编译器扩展)标准化
#embed
多线程 / 原子操作仅缺陷修复

8. 值不值得用

C17 没有任何负担,直接换 -std=c17 即可。C23 建议"新代码优先、渐进采用":bool 关键字、nullptr、typeof、数字分隔符、[[nodiscard]] 这些特性风险极低、收益立竿见影,新项目完全可以开 -std=c2x 来写;但如果你要发布给第三方编译的库,或目标平台工具链老旧,保持 C11 兼容写法仍是负责任的选择。总体而言,C23 让 C 语言重新变得"好用",值得每个 C 程序员保持关注——毕竟它定下了未来十年 C 代码的基本面貌。如果你正在规划新项目的技术选型,可以多留意 GCC 14 之后的更新日志,C23 特性仍在持续补齐中。

💡 学习建议:如果你的 GCC 在 13 以上,把一个小工具源码用 -std=c2x 重新编译,试着把 NULL 换成 nullptr、把 #define 常量换成 constexpr、给必须检查返回值的函数加上 [[nodiscard]],对比一下编译警告的变化,你会直观感受到"现代 C"的质感。