C23 上手:五个最有感的新特性
上手 C23 对日常写代码最有感的五个特性——数字分隔符、nullptr、bool 关键字、auto 与 [[attributes]],用 gcc -std=c23 验证标准级别,并分清「标准没开」与「编译器太旧」两类报错。
前置知识
- 已完成 变量与常量:会声明变量、用过
const,知道宏能做文本替换。本文的 nullptr、auto 都建立在「宏与真类型的区别」上; - 会编译运行一个 C 程序(零基础起步 已带)。零基础起步见 C 语言零基础起步。
分工说明:520 与 530 合讲 C 新标准。本篇是主教学,解决「日常写代码的新手感」:让编译器进入 C23 模式、五个立刻改变手感的特性、报错时分清标准没开还是编译器太旧;C23 深水区 负责编译期深水能力(#embed、typeof、constexpr、ckd_*)、C2y 草案现状与升级策略。两篇示例不重复,本篇是 530 的地基。
学习目标
读完本文你将能够:
- 用
gcc -std=c23编译程序,并用__STDC_VERSION__的输出证明标准生效; - 用数字分隔符与二进制字面量重写可读性差的长数字和位掩码;
- 不引头文件直接使用
bool/true/false,并用nullptr替代NULL; - 用
auto让编译器从初始化器推断类型,说出它与 C++ 的auto的两处不同; - 用
[[nodiscard]]、[[deprecated]]把约定变成编译器警告,并读懂警告原文。
预计 45 到 60 分钟,含 1 组修改实验、2 个调试实录与 3 道练习。
1. 问题引入
C 语言上一次大更新还要追溯到 1999 年的 C99——此后二十多年,这门语言几乎原地踏步,C17 只是一次缺陷修订。直到 C23(正式名称 ISO/IEC 9899:2024,2024 年发布)让它重新现代起来。看一个给游戏存档工具补统计功能的下午,你会接连撞上三件小别扭:
#define MAX_SCORE 1000000 /* 一百万:数零数到眼花,多一个零没人发现 */
#include <stdbool.h> /* 想写个布尔标志,还要专门引头文件 */
if (slot != NULL) { ... } /* 空指针判断:NULL 是宏?是 0?说不清 */根源是同一件事:C 的关键字太少,很多本该属于语言的能力长期靠宏硬撑。C23 一次还清了一部分。本文挑出对日常写代码最有感的五个,逐一上手。
2. 先让编译器说 C23
C23 特性不是「装了新编译器就有」,多数要显式打开标准开关。先做个探针,确认编译器现在按哪个标准工作:
/* check_std.c:打印编译器的标准身份证 */
#include <stdio.h>
int main(void) {
printf("__STDC_VERSION__ = %ld\n", (long)__STDC_VERSION__);
return 0;
}gcc --version | head -1
gcc -std=c23 check_std.c -o check_std && ./check_std预期输出(版本号以你的机器为准):
gcc (GCC) 14.2.0
__STDC_VERSION__ = 202311L202311L 就是 C23 的编号;不加 -std=c23 时,GCC 14 默认是 gnu17,会打印 201710L。用 #if __STDC_VERSION__ >= 202311L 做探测,让同一份代码在新旧编译器上自己选路,而不是赌编译器。
3. 特性一:数字分隔符,长数字不再数零
/* digit_sep.c:一百万到底是几个零 */
#include <stdio.h>
int main(void) {
long budget = 1'000'000; /* 一百万,三位一撇 */
unsigned mask = 0b1010'1010; /* 位掩码按 4 位分组 */
printf("budget = %ld\n", budget);
printf("mask = %u\n", mask);
return 0;
}gcc -std=c23 digit_sep.c -o digit_sep && ./digit_sep预期输出:
budget = 1000000
mask = 170分隔符 ' 不改变数值,纯粹给人看,而且编译器会检查它的位置——写错位置直接编译报错,比注释靠谱。写位掩码时(位运算 的日常),0b1010'1010 比 0xAA 更能看出「哪几位开了」。
4. 特性二:nullptr,空指针不再借用整数 0
NULL 是 <stddef.h> 里的宏,本质是整数 0(或 (void*)0);nullptr 是 C23 的关键字,有专属类型 nullptr_t,只能赋给指针:
/* nullptr_kw.c:空指针语义,编译器看得懂 */
#include <stdio.h>
void load_save(int *slot) {
if (slot == nullptr) {
puts("无存档,从零开始");
return;
}
printf("进度: %d 分\n", *slot);
}
int main(void) {
int score = 850;
load_save(nullptr);
load_save(&score);
return 0;
}预期输出:
无存档,从零开始
进度: 850 分NULL 依然可用,旧代码离不开它;但 nullptr 把「这是空指针」写进了类型系统:int x = nullptr; 在 C23 下直接编译报错。这是限制,更是保护——整数和空指针从此不再被混用。
5. 特性三:bool/true/false 成为关键字
C23 之前,bool 是 <stdbool.h> 里的宏,true/false 也是宏。现在它们是真正的关键字,直接可用:
/* bool_kw.c:注意开头,没有 stdbool.h */
#include <stdio.h>
bool is_even(int n) {
return n % 2 == 0;
}
int main(void) {
bool game_over = false;
printf("4 是偶数: %d\n", is_even(4));
printf("sizeof(bool) = %zu\n", sizeof(bool));
return 0;
}预期输出:
4 是偶数: 1
sizeof(bool) = 1bool 打印出来是 1/0,占 1 个字节(不是 1 位;按位打包用位域,见 位域)。<stdbool.h> 仍保留着兼容旧代码,新代码可以直接不写。
6. 特性四:auto,让编译器替你写类型
auto 在 C23 里改了含义:从「自动存储期」变成「从初始化器推断类型」。类型名冗长时特别省事:
/* auto_infer.c:类型名不用手写第二遍 */
#include <stdio.h>
struct Player { char name[16]; int level; };
struct Player *find_player(void) {
static struct Player p = { "Lin", 12 };
return &p;
}
int main(void) {
auto score = 980; /* int */
auto ratio = 0.5; /* double */
auto who = find_player(); /* struct Player * */
printf("%s: %d (%.2f)\n", who->name, score, ratio);
return 0;
}预期输出:
Lin: 980 (0.50)两个与 C++ 的关键差异:必须有初始化器(auto x; 编译报错);不能用于函数返回类型推导(auto f(void) {...} 不是合法的 C23)。C23 的 auto 只做一件事:把等号右边的类型搬到左边。
7. 特性五:[[attributes]],把约定交给编译器检查
团队约定里最常被违反的两条:「这个函数别再用了」「这个返回值不能丢」。属性语法让编译器替你盯着:
/* attrs.c:约定写进代码,编译器执行 */
#include <stdio.h>
[[nodiscard]] int calc_combo(int hits) { /* 返回值不许丢 */
return hits * 10;
}
[[deprecated("用 calc_combo")]]
int old_combo(int hits) { /* 旧接口,标记弃用 */
return hits * 10;
}
int main(void) {
printf("combo = %d\n", calc_combo(8));
return 0;
}预期输出:
combo = 80程序正常、零警告——因为你没违反约定。标准属性还有 [[fallthrough]](标注 switch 有意穿透)、[[maybe_unused]](压掉未使用警告)、[[noreturn]](函数不返回)。更多属性见 属性与编译器扩展。
8. 核心概念:C23 现代化的三条线
- 把宏转正为关键字:
bool/nullptr的语义进入类型系统,编译器能替你查错,而不是靠宏展开后的巧合正确; - 减少重复劳动:数字分隔符让人不再数零,
auto让类型名只写一遍——省的是出错机会; - 把约定变成可检查的规则:
[[attributes]]让违反约定的代码在编译期暴露,无需等人 code review。
C23 一共改了几十处,本文只挑「写了立刻有感」的五个。编译期更强的能力,深水区 接着讲。
9. 修改实验:分隔符只是给人看的
把 digit_sep.c 里 budget 一行改成下面这样(分隔符位置故意不按三位一组),先预测输出再运行:
long budget = 1'00'0000;预期输出(先写下你的答案):budget = 1000000,mask = 170——数值完全没变。分隔符可以插在数字任意位置,编译器只要求「在数字中间」。顺手把 mask 改成 0b1111'0000,先心算十进制(240),再运行验证。
10. 常见错误与调试实录
实录一:error: unknown type name ‘bool’——忘了开标准
把 bool_kw.c 交给一个默认 C17 的构建命令:
gcc bool_kw.c -o bool_kw /* 没有 -std=c23,默认 gnu17 */真实报错(截取关键行):
bool_kw.c:3:1: error: unknown type name 'bool'; did you mean '_Bool'?
3 | bool is_even(int n) {
| ^~~~
| _Bool
bool_kw.c:8:20: error: 'false' undeclared (first use in this function)读报错三步:unknown type name 'bool' 说明编译器不认识这个类型名;但代码写法没错,是「代码要求的标准」和「编译器当前标准」不匹配;看编译命令,没有 -std=c23,加上再编即消失。GCC 提示的 _Bool 是 C99 老写法,不加开关时它是退路。
实录二:warning: … declared with attribute ‘nodiscard’——你丢了返回值
把 attrs.c 的 main 里那行 printf 拆开,改成只调用、不接收:calc_combo(8);。重新编译,真实警告:
attrs.c:14:5: warning: ignoring return value of 'calc_combo' declared with attribute 'nodiscard' [-Wunused-result]
14 | calc_combo(8);
| ^~~~~~~~~~~~~读报错三步:这是 warning 不是 error,程序照常生成——很多人因此当没看见,这正是它危险的地方;ignoring return value ... 'nodiscard' 指出你调用了 [[nodiscard]] 函数却没用返回值;真要丢弃(只关心副作用)就显式写 (void)calc_combo(8); 让意图可见,否则接住返回值。
排障速查与版本表
看到 unknown type name 'bool'、'nullptr' undeclared 这类错,先怀疑标准没开(加 -std=c23),仍报错再怀疑编译器太旧——按下表对照(出处:cppreference C23 支持页,验证于 2026-09):
| 特性 | GCC 最低 | Clang 最低 |
|---|---|---|
| 数字分隔符 | 12 | 13 |
| nullptr、bool/true/false 关键字 | 13 | 16 |
| auto 类型推断 | 13 | 18 |
| [[attributes]] | 10 | 9 |
GCC 15 起默认标准已是 gnu23(以发布说明为准);MSVC 的 C23 支持仍在推进中,以 Microsoft 文档为准。
11. 实际项目中的使用场景
- 固件与嵌入式(如 FoloToy-calendar 这类 ESP32 项目):
0b位掩码对着芯片手册写寄存器,见 嵌入式 C 编程; - 跨平台库与接口治理:开头一段
__STDC_VERSION__探测让同一份源码在新旧编译器上都编译通过;[[deprecated]]标记旧接口,「换新函数」从口头约定变成编译器统计; - 需要兼容老工具链的第三方 SDK 就老实用
-std=c17:新特性是工具,不是炫技。
12. 小练习
预测题(5 分钟):先写下答案再运行验证:
auto x = 3.0 / 2;
printf("%d\n", x);参考答案(先写再看):x 是 double(3.0/2 是浮点除法)。用 %d 打印 double 是未定义行为;-Wall 下会看到 warning: format '%d' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]。
修改题(15 分钟):在 bool_kw.c 基础上新增 bool is_leap(int year) 判断闰年(能被 4 整除但不能被 100 整除,或能被 400 整除),main 里依次验证 2000、1900、2024。验收:输出 1 0 1 三行;全程不出现 #include <stdbool.h>。
修 Bug 题(15 分钟):下面这份代码在 CI 上编译失败,真实报错如下。按「读报错三步」定位并给出两种修复:
/* buggy_std.c */
#include <stdio.h>
struct Save { int level; };
void print_save(struct Save *s) {
if (s == nullptr) {
puts("empty");
return;
}
printf("level %d\n", s->level);
}
int main(void) {
print_save(nullptr);
return 0;
}buggy_std.c:6:14: error: 'nullptr' undeclared (first use in this function)
6 | if (s == nullptr) {
| ^~~~~~~
buggy_std.c:6:14: note: each undeclared identifier is reported only once for each function it appears in
...
buggy_std.c:13:16: error: 'nullptr' undeclared (first use in this function)参考要点:构建用的是 -std=c17,C17 没有这个关键字。修复一(首选):构建改 gcc -std=c23(需 GCC 13+);修复二(保底):改回 NULL 并 #include <stddef.h>。选哪种,取决于团队允不允许动构建脚本。
13. 与之前和之后的知识的关系
- 往前:变量与常量 里
const与#define的分工,正是深水区constexpr要接手的话题;位运算与位域 的掩码用0b1010'1010写,可读性上一个台阶; - 往后:C23 深水区 回答本文按下不表的「C2y 是什么状态」「老项目怎么迁到 C23」;
- 更远:
[[attributes]]只是标准属性最小集,编译器专属属性大家族在 属性与编译器扩展;-std=c23写进构建脚本见 构建系统。
14. 官方文档
- cppreference C23 编译器支持(本文版本事实出处,验证于 2026-09):https://en.cppreference.com/w/c/compiler_support/23
- WG14 官方文档库(标准草案与提案原文):https://www.open-std.org/jtc1/sc22/wg14/
- GCC 手册 C 方言选项(
-std全部取值):https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html
15. 自我检查
- 能不看资料写出「打印
__STDC_VERSION__并判断是否为 C23」的探针程序; - 能一口气说出五个特性的名字,并各用一句话讲清它解决了什么旧别扭;
- 看到
unknown type name 'bool'能立刻给出两个可能原因并按顺序排查; - 能说清 C23 的
auto与 C++ 的auto至少两处不同。
本章总结
C23(ISO/IEC 9899:2024)结束了 C 语言二十多年的原地踏步。最有感的五件事:数字分隔符让长数字可数,nullptr 让空指针有类型,bool/true/false 不再借住头文件,auto 让类型名只写一遍,[[attributes]] 让约定变成编译警告。开关是 gcc -std=c23,身份证是 __STDC_VERSION__ = 202311L,排障第一步永远是分清「标准没开」还是「编译器太旧」。编译期更强的深水特性,下一篇见。
下一步
进入 C23 深水区:把资源文件编进可执行文件,写出类型安全的泛型宏,拦下整数溢出,然后回答两个工程问题——怎么查编译器支持矩阵,老代码库怎么一步步升到 C23。