C 标准演进、特性检测与可移植性
本节目标
查询 C11、C17、C23 标准版本、特性检测和可移植性边界。
可移植性不是找到一个“足够新”的编译器版本号,而是逐层确认翻译模式、执行环境、条件能力、实现选择和项目实际使用的接口。本章给出从 C11、C17 到 C23 的判断顺序,并用一个只读取标准宏的探测程序展示如何记录当前实现;预处理与条件编译的基本方法见条件编译与特性检测。
C11、C17、C23 基线地图
C 标准版本是源代码与实现之间的语言和库合同,不等于某个编译器产品的发布日期或主版本号。C11 是引入原子、标准线程等机制的一次重要修订;C17 主要是缺陷修正与澄清版本,不应被描述为新增大批语言特性;C23 又调整并增加了语言与库能力。具体能力仍须结合所选翻译模式、条件宏和实现文档确认,不能从“支持 C23”推导出每项接口都在目标环境可用。
本章的版本边界以 WG14 公开文本 WG14 N1570、WG14 N2176 和 WG14 N3096 为一手依据。N2176 是 WG14 为 C17 投票准备的工作草案,用它描述 C17 边界,不把它称为正式发布的 ISO/IEC 9899:2018 文档;遇到草案中的待定年月形式或正式版书目信息时,应明确这层证据边界。
| 基线 | 适合作为迁移问题 | 不应推出的结论 |
|---|---|---|
| C11 | 旧代码是否依赖更早语法,是否要接入 C11 语言或条件库能力 | 所有实现都提供线程、原子和 VLA |
| C17 | 能否在以修正和澄清为主的基线上稳定构建 | 编译器产品更新就自动新增一批 C17 语法 |
| C23 | 新语法、头文件和库接口是否有明确收益与降级方案 | 接受 -std=c23 就代表目标库与执行环境完整支持所有能力 |
选择基线时先写出项目真正需要的语法、头文件和运行时能力,再分别用编译测试和目标运行测试验证。版本标签用于限定候选合同,不替代能力清单。
一致性实现与实现文档
严格一致程序只使用标准规定的语言和库特性,不让输出依赖未指定、未定义或实现定义行为,也不超过任何最低实现限制。实际工程可以有意识地使用实现扩展,但应把扩展隔离在小范围适配层,并记录启用条件和标准后备路径。实现定义、未指定和未定义行为不是同一种可移植性边界;三者的处理方式如下。
| 边界 | 实现承担什么 | 项目动作 |
|---|---|---|
| 实现定义行为 | 实现选择一种结果并记录选择 | 查目标实现文档,把选择写入支持矩阵并测试 |
| 未指定行为 | 标准允许若干可能结果,通常不要求实现记录本次选择 | 不让正确性依赖其中某一个结果 |
| 未定义行为 | 标准不规定程序行为 | 修改程序,不能靠本机测试把它“验证”为可用 |
| 实现扩展 | 实现另行提供语法、宏、头文件或接口 | 用严格模式暴露依赖,在适配层提供禁用或替换路径 |
审查一个实现相关结论时,按“标准版本条款 → 实现文档 → 最小编译/运行探测”保存证据。对象行为的三个类别与例子可回看未定义、未指定与实现定义行为。
__STDC__ 与 __STDC_VERSION__
__STDC__ 用于识别实现对标准 C 翻译的声明,__STDC_VERSION__ 则给出当前翻译模式对应的版本长整型常量。__STDC_VERSION__ 描述当前翻译模式宣称的标准版本,而不是编译器产品版本;同一编译器用不同 -std 选项翻译时,结果可以不同。
N1570、N2176 与 N3096 的强制预定义宏条款都规定,__STDC__ 的标准值是整数常量 1,意在表明一致实现。版本宏则要连同来源阅读:N3096 的变更历史给出前三个版本边界的最终数值,而 N1570 仍保留草案中的待定年月形式 201ymmL,不能把它误抄成实际宏值。
| 标准模式边界 | __STDC_VERSION__ | 来源限定与读法 |
|---|---|---|
| C11 | 201112L | N3096 附录 M 记录第三版边界;N1570 6.10.8.1 自身仍写待定年月形式 201ymmL |
| C17 | 201710L | N2176 6.10.8.1 的投票草案值;N3096 附录 M 将其列为第四版边界 |
| C23 | 202311L | N3096 6.10.9.1 的工作草案值,也是其附录 M 所列第五版边界 |
这些数值用于判断当前实现为实际选择的翻译模式作出的声明,不证明产品的所有前端、标准库、链接器或目标环境能力都已同步实现。
| 观察 | 可作出的判断 | 仍需确认 |
|---|---|---|
定义了 __STDC__ | 当前预处理环境给出了标准 C 标识 | 严格模式选项、扩展是否开启、目标库能力 |
定义了 __STDC_VERSION__ | 可把其值与所需标准版本宏值比较 | 具体语言/库能力是否为条件特性,头文件是否可用 |
未定义 __STDC_VERSION__ | 不能用它证明 C11 或更晚的翻译模式 | 实现是否处在更早模式或非标准模式 |
版本比较只适合守卫确实由某次标准修订引入且无条件提供的接口。产品版本宏(例如供应商自定义宏)只能描述实现产品,不能代替标准版本宏;需要供应商扩展时,将判断封装到构建配置或单独兼容头中。
hosted 与 freestanding
__STDC_HOSTED__ 区分 hosted 与 freestanding 实现。hosted 环境提供标准规定的宿主程序启动和库环境;freestanding 环境的启动方式与可用库范围更受实现约定支配。这个宏回答执行环境类别,不回答操作系统名称,也不能证明某个非标准设备接口存在。
本探针中 hosted 的值域是 0、1 或 unknown:宏存在时原样记录标准规定的 0 或 1,宏缺失时记录 unknown,不再把“无法分类”误写成 freestanding。
__STDC_HOSTED__ 的观察 | 入口与库假设 | 下一步 |
|---|---|---|
值为 1 | 可以按所选标准的 hosted 合同设计程序 | 仍检查条件能力、链接选项和运行环境 |
值为 0 | 不假定常规 main 启动流程或完整 hosted 库 | 查实现的启动、终止、I/O 和可用头文件文档 |
| 宏异常缺失 | 探针不能分类当前环境 | 把目标视为未确认,并用实现文档和编译测试补证据 |
freestanding 的“最低库”也随标准版本变化,不能把 C23 清单倒推到 C11/C17:
| 来源与模式 | 一致 freestanding 实现必须接受的最低库使用 |
|---|---|
| N1570(C11) | <float.h>、<iso646.h>、<limits.h>、<stdalign.h>、<stdarg.h>、<stdbool.h>、<stddef.h>、<stdint.h>、<stdnoreturn.h> |
| N2176(C17 工作草案) | <float.h>、<iso646.h>、<limits.h>、<stdalign.h>、<stdarg.h>、<stdbool.h>、<stddef.h>、<stdint.h>、<stdnoreturn.h> |
| N3096(C23) | 在上述九个头的基础上增加 <stdbit.h>;还包括 <string.h> 中除 strdup、strndup、strcoll、strxfrm、strerror 外的设施,以及 <stdlib.h> 中选定的 memalignment |
N3096 对定义相应 IEC 60559 宏时还有条件性最低保证:实现还要接受使用 <fenv.h>、<math.h> 和 <stdlib.h> 中 strto* 浮点转换函数的相应严格一致程序,前提包括不把 FENV_ACCESS 设为 ON。这类条件扩展仍不能推出 <stdio.h>、进程入口或其他 hosted 设施可用。
移植到固件或内核类目标时,先列出入口、堆、流 I/O、时间、线程和终止语义六项需求,再逐项映射到实现提供的接口。不要用一次桌面 hosted 运行替代目标机验证。
条件特性与 __STDC_NO_*__
条件特性宏是彼此独立的负向声明,不是一个总开关。__STDC_NO_ATOMICS__、__STDC_NO_THREADS__ 和 __STDC_NO_VLA__ 必须分别按对应版本条款解释:宏存在时处理它所命名能力的缺失;宏未定义时也只对该项能力作判断,不能顺带推导另外两项。
| 宏 | 探针记录 | 工程动作 |
|---|---|---|
__STDC_NO_ATOMICS__ | 定义为 unavailable;未定义且 hosted 为 available;否则为 unknown | 分别验证原子类型、头文件与所需操作;缺失时选择无并发后备或目标同步原语 |
__STDC_NO_THREADS__ | 定义为 unavailable;未定义且 hosted 为 available;否则为 unknown | 编译所需标准线程接口;缺失时禁用功能或隔离平台线程适配层 |
__STDC_NO_VLA__ | 定义为 unavailable,否则为 available | 按当前标准版本确认该宏对应的 VLA 范围,优先准备固定容量或动态分配路径 |
__STDC_NO_ATOMICS__ 未定义时,只有已确认的 hosted 实现才记为 available;freestanding 或无法分类的实现记为 unknown。原因是 freestanding 的无条件最低库不包含 <stdatomic.h>,此时仍需实现文档和最小编译测试确认原子类型、头文件与操作。
__STDC_NO_THREADS__ 未定义时同样只在 hosted 合同下记为 available;freestanding 或无法分类时记为 unknown。freestanding 的最低库不保证 <threads.h>,而且是否有多个执行线程也由实现定义,因此还要验证头文件、链接与目标运行时。
C11/C17 中,__STDC_NO_VLA__ == 1 表示实现不支持 VLA 或 variably modified types(可变修改类型),因此这两个模式下的 vla 状态覆盖两者。C23 中,本探针的 vla 键只表示自动存储期 VLA 对象支持:即使 __STDC_NO_VLA__ == 1,数组形参仍会调整为指针类型,VLA 参数声明仍是必须支持的。代码和能力矩阵都应同时记录所选翻译模式,不能跨版本复用同一句解释。
不要把 #if __STDC_VERSION__ >= ... 当作条件能力检测。原子和线程的具体接口边界见信号、原子操作与标准线程;VLA 的替代方案应同时说明容量上限、分配失败和对象生命周期。
翻译限制、整数范围与数据模型
ISO C 规定最低要求和类型关系,但具体范围、宽度与翻译限制由目标实现落实。不能把本机的 LP64 或 LLP64 数据模型写成语言保证:两者对 long 与指针宽度的选择不同,其他实现还可能采用别的数据模型。
| 需要的事实 | 首选证据 | 可移植写法 |
|---|---|---|
| 基本整数范围、每字节位数 | <limits.h> 的 INT_MIN、INT_MAX、CHAR_BIT 等 | 用头文件宏判断范围,不硬编码 8 位字节或 32 位 int |
| 精确宽度整数是否存在 | <stdint.h> 中相应 typedef 和限制宏 | 只在相应名字可用时使用;否则按最小宽度或范围设计接口 |
| 对象大小 | sizeof、SIZE_MAX 及目标 ABI 文档 | 用 size_t 表示对象大小并检查乘加溢出 |
| 翻译限制 | 所选标准的最低要求与实现文档 | 生成代码、嵌套层数或标识符长度接近边界时加入编译探测 |
跨 ABI 的文件或网络格式要定义字段位数和编码,再显式转换;内存中的 struct 大小、指针大小和 long 范围不能直接充当外部格式。基础类型范围与定宽整数选择见大小与范围。
对象表示、字节序与对齐
对象的值表示只承载值,完整对象表示还可能包含填充位或填充字节。多字节对象的字节序由实现选择,结构成员之间和末尾也可能有填充;类型的对齐要求应通过语言设施和实现 ABI 获得,不能从成员宽度手算偏移。
| 风险操作 | 失败假设 | 可移植替代 |
|---|---|---|
| 把整数对象的原始字节直接写入文件 | 所有目标字节序相同且没有表示差异 | 定义外部字节顺序,逐字段编码/解码 |
用 memcmp 判断任意结构值相等 | 填充字节稳定且值相等意味着表示逐字节相同 | 逐成员按值比较 |
| 强制转换外部字节地址后解引用 | 目标允许该对齐、字节序、别名访问和对象表示 | 按协议读取无符号字节,通过移位与范围检查组合出目标值 |
| 用字段大小相加推导结构布局 | 没有成员间或尾部填充 | 使用 sizeof、对齐查询和已记录的目标 ABI |
外部文件或网络字节必须按协议逐字节解码并组合成目标值,解码过程要显式处理字节序、符号和范围。memcpy 只适合复制已知与目标类型兼容的有效对象表示,例如在同一实现中复制一个有效对象的表示;memcpy 不能修复字节序,也不能把可能的非值表示转换成有效值。把任意外部字节复制进整数或浮点对象后读取,仍可能得到错误值或触发非值表示边界。
字符类型观察与 memcpy 的边界见对象表示、值表示与填充字节,有效类型和别名规则见字符类型访问与 memcpy。
头文件和库接口的可移植边界
一个头文件名称能否使用,取决于所选标准版本、hosted/freestanding 类别以及该能力是否为条件支持。实现扩展头文件和平台 SDK 即使在多家编译器中同名,也不会因此成为 ISO C 标准头文件;扩展接口的名字、类型、错误合同和链接需求都应来自实现文档。
| 接口类别 | 识别方法 | 代码组织 |
|---|---|---|
| 所选模式要求的标准头 | 对照该版本的标准头清单并实际编译 | 直接包含,保持头文件自包含 |
| 条件能力对应的头和接口 | 先解释对应条件宏,再编译所需声明 | 功能模块整体启用或走明确降级路径 |
| 后续标准新增接口 | 以翻译模式和该接口的标准条件为准 | 兼容层提供同语义后备,不仅猜产品版本 |
| 实现扩展头或平台 SDK | 以实现/平台官方文档和构建探测为准 | 集中到适配层,不把扩展标识符泄漏进公共接口 |
避免自行声明库函数,也不要借助下划线开头等保留标识符伪造能力宏。标准库分区可查标准库头文件与任务地图,公开头的自包含规则可查自包含头文件与接口契约。
编译器扩展与严格一致性模式
严格模式用于暴露代码对实现扩展的依赖;警告提升为错误可以让迁移期的偏差更早进入审查,但某组命令行选项的具体含义仍由编译器文档定义。扩展模式适合明确受控的目标,不适合被描述成严格一致程序的最低需求。
| 检查阶段 | 操作 | 通过标准 |
|---|---|---|
| 最低基线 | 用项目声明的最早标准模式和高等级诊断编译 | 不需要更新标准或供应商扩展即可构建核心路径 |
| 最新基线 | 用目标 C23 模式编译同一代码 | 新模式下无新增诊断,行为测试保持一致 |
| 扩展审计 | 搜索供应商宏、属性、内建函数和扩展头 | 每项都有适配层、目标列表与禁用路径 |
| 目标验证 | 使用实际编译器、库、链接器和运行环境 | 编译、链接和运行证据来自同一目标组合 |
不要把“没有警告”解释为标准一致性证明:诊断只覆盖实现检测到且承诺报告的部分,运行期前置条件、未定义行为与 ABI 假设还需要代码审查和测试。诊断工具的证据边界见诊断工具选择地图。
能力探测程序及输出解释
下面的完整程序只观察标准宏,不根据供应商或产品版本猜测能力。七个键保持固定顺序,便于把不同翻译模式和目标环境的结果放入能力矩阵。
#include <stdio.h>
#define STRINGIFY_INNER(value) #value
#define STRINGIFY(value) STRINGIFY_INNER(value)
#if defined(__STDC__)
#define STDC_VALUE 1
#else
#define STDC_VALUE 0
#endif
#if defined(__STDC_VERSION__)
#define STDC_VERSION_TEXT STRINGIFY(__STDC_VERSION__)
#else
#define STDC_VERSION_TEXT "undefined"
#endif
#if defined(__STDC_HOSTED__)
#define HOSTED_STATUS STRINGIFY(__STDC_HOSTED__)
#else
#define HOSTED_STATUS "unknown"
#endif
#if defined(__STDC_NO_ATOMICS__)
#define ATOMICS_STATUS "unavailable"
#elif defined(__STDC_HOSTED__) && __STDC_HOSTED__ == 1
#define ATOMICS_STATUS "available"
#else
#define ATOMICS_STATUS "unknown"
#endif
#if defined(__STDC_NO_THREADS__)
#define THREADS_STATUS "unavailable"
#elif defined(__STDC_HOSTED__) && __STDC_HOSTED__ == 1
#define THREADS_STATUS "available"
#else
#define THREADS_STATUS "unknown"
#endif
#if defined(__STDC_NO_VLA__)
#define VLA_STATUS "unavailable"
#else
#define VLA_STATUS "available"
#endif
#if defined(__STDC_IEC_60559_BFP__)
#define IEC_60559_BFP_STATUS "available"
#else
#define IEC_60559_BFP_STATUS "unknown"
#endif
int main(void) {
printf("stdc=%d\n", STDC_VALUE);
printf("stdc_version=%s\n", STDC_VERSION_TEXT);
printf("hosted=%s\n", HOSTED_STATUS);
printf("atomics=%s\n", ATOMICS_STATUS);
printf("threads=%s\n", THREADS_STATUS);
printf("vla=%s\n", VLA_STATUS);
printf("iec_60559_bfp=%s\n", IEC_60559_BFP_STATUS);
return 0;
}
当前实现输出(实现特定):
stdc=1
stdc_version=202311L
hosted=1
atomics=available
threads=unavailable
vla=available
iec_60559_bfp=unknown
当前探测输出只代表运行测试的实现。它是在当前工具链的严格 C23 翻译模式下编译并运行所得,不是其他平台应复制的期望值。feature_probe.c 本身包含 <stdio.h> 并调用 printf,只能在实现提供这两项设施时编译并运行;它们不属于所有 freestanding 版本的无条件最低保证。因此 hosted=0 不能证明探针已在每个 freestanding 运行环境成功执行,只说明某次确实能构建并执行探针的环境把宏值报告为 0。解释时按下面的顺序逐行进行:
stdc与stdc_version记录翻译模式声明,不证明所有条件能力存在。hosted只分类执行环境,不能替代对单个库接口的编译与链接测试。atomics、threads、vla分别来自对应__STDC_NO_*__分支,必须独立阅读。iec_60559_bfp=available只在对应宏存在时输出;宏缺失记为unknown,不能反写成“不支持”。
探针适合生成候选能力矩阵,最终结论还应包含编译命令、目标三元组、库/SDK 版本和最小接口测试。结构化输出的键可以稳定,值必须允许目标实现不同。
C11/C17 向 C23 迁移清单
迁移的目标不是让版本数字变大,而是让每项依赖都有证据和降级路径。按以下清单执行,可以避免只靠预处理器版本猜测功能。
- 列出当前代码实际使用的语法、标准头、库接口、扩展和 ABI 假设,并标明拥有者。
- 在声明的 C11 或 C17 最低模式下建立干净的严格编译基线,再在 C23 模式下比较新增诊断。
- 对每个条件能力读取独立宏,并为真正调用的接口增加最小编译、链接或运行探测。
- 对后续标准能力先验证接口本身;无法使用时选择等语义后备、禁用该模块或由构建系统生成明确的配置宏。
- 用
<limits.h>、<stdint.h>、sizeof、对齐查询和实现文档替换固定 LP64/LLP64、字节序或结构布局假设。 - 在每个受支持目标运行行为测试和数据交换测试,记录编译器、标准库、链接器与执行环境的组合。
后备实现必须保持同一公开契约,包括范围、错误、所有权和线程安全;若做不到,应让配置显式失败,而不是静默改变语义。测试层次与接口合同可参考自动化测试、接口设计、风格与文档。
常见陷阱
可移植性故障通常不是“编译器太旧”这一种原因,而是把某层观察错误地推广成另一层保证。审查时用下表把失败假设替换为可重复的证据。
| 失败假设 | 为什么失败 | 可移植替代 |
|---|---|---|
| 编译器产品版本足够新,所以一定支持 C23 | 产品版本不等于当前翻译模式,语言与库支持也可能分阶段 | 检查 __STDC_VERSION__,再对实际接口做编译/运行探测 |
一个 __STDC_NO_*__ 未定义,所以全部条件能力可用 | 三个条件宏彼此独立 | 分别读取宏,并分别测试原子、线程和 VLA 路径 |
| 本机是 LP64,所以整数、指针和外部格式都固定 | C 不规定项目采用 LP64,其他目标可能是 LLP64 或其他模型 | 从标准头、sizeof 和实现 ABI 取值,外部格式显式编码 |
| 结构可直接逐字节比较或发送 | 字节序、填充、对齐和对象表示可能不同 | 逐成员比较,按协议逐字段序列化 |
| 能包含扩展头就说明它是标准接口 | 实现扩展不会因常见而变成 ISO C 保证 | 隔离平台适配层并提供标准后备或明确目标约束 |
| 严格编译无警告就证明程序可移植 | 诊断不能证明所有运行期前置条件和 ABI 假设 | 组合标准审查、实现文档、静态诊断和多目标行为测试 |
最后追问三件事:这条结论属于标准保证、实现记录还是本次探测?换一个翻译模式会不会变化?缺失时程序是降级、禁用还是拒绝构建?三问都能明确回答,能力声明才可进入支持矩阵。