跳到主要内容

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__来源限定与读法
C11201112LN3096 附录 M 记录第三版边界;N1570 6.10.8.1 自身仍写待定年月形式 201ymmL
C17201710LN2176 6.10.8.1 的投票草案值;N3096 附录 M 将其列为第四版边界
C23202311LN3096 6.10.9.1 的工作草案值,也是其附录 M 所列第五版边界

这些数值用于判断当前实现为实际选择的翻译模式作出的声明,不证明产品的所有前端、标准库、链接器或目标环境能力都已同步实现。

观察可作出的判断仍需确认
定义了 __STDC__当前预处理环境给出了标准 C 标识严格模式选项、扩展是否开启、目标库能力
定义了 __STDC_VERSION__可把其值与所需标准版本宏值比较具体语言/库能力是否为条件特性,头文件是否可用
未定义 __STDC_VERSION__不能用它证明 C11 或更晚的翻译模式实现是否处在更早模式或非标准模式

版本比较只适合守卫确实由某次标准修订引入且无条件提供的接口。产品版本宏(例如供应商自定义宏)只能描述实现产品,不能代替标准版本宏;需要供应商扩展时,将判断封装到构建配置或单独兼容头中。

hosted 与 freestanding

__STDC_HOSTED__ 区分 hosted 与 freestanding 实现。hosted 环境提供标准规定的宿主程序启动和库环境;freestanding 环境的启动方式与可用库范围更受实现约定支配。这个宏回答执行环境类别,不回答操作系统名称,也不能证明某个非标准设备接口存在。

本探针中 hosted 的值域是 01unknown:宏存在时原样记录标准规定的 01,宏缺失时记录 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> 中除 strdupstrndupstrcollstrxfrmstrerror 外的设施,以及 <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_MININT_MAXCHAR_BIT用头文件宏判断范围,不硬编码 8 位字节或 32 位 int
精确宽度整数是否存在<stdint.h> 中相应 typedef 和限制宏只在相应名字可用时使用;否则按最小宽度或范围设计接口
对象大小sizeofSIZE_MAX 及目标 ABI 文档size_t 表示对象大小并检查乘加溢出
翻译限制所选标准的最低要求与实现文档生成代码、嵌套层数或标识符长度接近边界时加入编译探测

跨 ABI 的文件或网络格式要定义字段位数和编码,再显式转换;内存中的 struct 大小、指针大小和 long 范围不能直接充当外部格式。基础类型范围与定宽整数选择见大小与范围

对象表示、字节序与对齐

对象的值表示只承载值,完整对象表示还可能包含填充位或填充字节。多字节对象的字节序由实现选择,结构成员之间和末尾也可能有填充;类型的对齐要求应通过语言设施和实现 ABI 获得,不能从成员宽度手算偏移。

风险操作失败假设可移植替代
把整数对象的原始字节直接写入文件所有目标字节序相同且没有表示差异定义外部字节顺序,逐字段编码/解码
memcmp 判断任意结构值相等填充字节稳定且值相等意味着表示逐字节相同逐成员按值比较
强制转换外部字节地址后解引用目标允许该对齐、字节序、别名访问和对象表示按协议读取无符号字节,通过移位与范围检查组合出目标值
用字段大小相加推导结构布局没有成员间或尾部填充使用 sizeof、对齐查询和已记录的目标 ABI

外部文件或网络字节必须按协议逐字节解码并组合成目标值,解码过程要显式处理字节序、符号和范围。memcpy 只适合复制已知与目标类型兼容的有效对象表示,例如在同一实现中复制一个有效对象的表示;memcpy 不能修复字节序,也不能把可能的非值表示转换成有效值。把任意外部字节复制进整数或浮点对象后读取,仍可能得到错误值或触发非值表示边界。

字符类型观察与 memcpy 的边界见对象表示、值表示与填充字节,有效类型和别名规则见字符类型访问与 memcpy

头文件和库接口的可移植边界

一个头文件名称能否使用,取决于所选标准版本、hosted/freestanding 类别以及该能力是否为条件支持。实现扩展头文件和平台 SDK 即使在多家编译器中同名,也不会因此成为 ISO C 标准头文件;扩展接口的名字、类型、错误合同和链接需求都应来自实现文档。

接口类别识别方法代码组织
所选模式要求的标准头对照该版本的标准头清单并实际编译直接包含,保持头文件自包含
条件能力对应的头和接口先解释对应条件宏,再编译所需声明功能模块整体启用或走明确降级路径
后续标准新增接口以翻译模式和该接口的标准条件为准兼容层提供同语义后备,不仅猜产品版本
实现扩展头或平台 SDK以实现/平台官方文档和构建探测为准集中到适配层,不把扩展标识符泄漏进公共接口

避免自行声明库函数,也不要借助下划线开头等保留标识符伪造能力宏。标准库分区可查标准库头文件与任务地图,公开头的自包含规则可查自包含头文件与接口契约

编译器扩展与严格一致性模式

严格模式用于暴露代码对实现扩展的依赖;警告提升为错误可以让迁移期的偏差更早进入审查,但某组命令行选项的具体含义仍由编译器文档定义。扩展模式适合明确受控的目标,不适合被描述成严格一致程序的最低需求。

检查阶段操作通过标准
最低基线用项目声明的最早标准模式和高等级诊断编译不需要更新标准或供应商扩展即可构建核心路径
最新基线用目标 C23 模式编译同一代码新模式下无新增诊断,行为测试保持一致
扩展审计搜索供应商宏、属性、内建函数和扩展头每项都有适配层、目标列表与禁用路径
目标验证使用实际编译器、库、链接器和运行环境编译、链接和运行证据来自同一目标组合

不要把“没有警告”解释为标准一致性证明:诊断只覆盖实现检测到且承诺报告的部分,运行期前置条件、未定义行为与 ABI 假设还需要代码审查和测试。诊断工具的证据边界见诊断工具选择地图

能力探测程序及输出解释

下面的完整程序只观察标准宏,不根据供应商或产品版本猜测能力。七个键保持固定顺序,便于把不同翻译模式和目标环境的结果放入能力矩阵。

feature_probe.c
#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。解释时按下面的顺序逐行进行:

  1. stdcstdc_version 记录翻译模式声明,不证明所有条件能力存在。
  2. hosted 只分类执行环境,不能替代对单个库接口的编译与链接测试。
  3. atomicsthreadsvla 分别来自对应 __STDC_NO_*__ 分支,必须独立阅读。
  4. iec_60559_bfp=available 只在对应宏存在时输出;宏缺失记为 unknown,不能反写成“不支持”。

探针适合生成候选能力矩阵,最终结论还应包含编译命令、目标三元组、库/SDK 版本和最小接口测试。结构化输出的键可以稳定,值必须允许目标实现不同。

C11/C17 向 C23 迁移清单

迁移的目标不是让版本数字变大,而是让每项依赖都有证据和降级路径。按以下清单执行,可以避免只靠预处理器版本猜测功能。

  1. 列出当前代码实际使用的语法、标准头、库接口、扩展和 ABI 假设,并标明拥有者。
  2. 在声明的 C11 或 C17 最低模式下建立干净的严格编译基线,再在 C23 模式下比较新增诊断。
  3. 对每个条件能力读取独立宏,并为真正调用的接口增加最小编译、链接或运行探测。
  4. 对后续标准能力先验证接口本身;无法使用时选择等语义后备、禁用该模块或由构建系统生成明确的配置宏。
  5. <limits.h><stdint.h>sizeof、对齐查询和实现文档替换固定 LP64/LLP64、字节序或结构布局假设。
  6. 在每个受支持目标运行行为测试和数据交换测试,记录编译器、标准库、链接器与执行环境的组合。

后备实现必须保持同一公开契约,包括范围、错误、所有权和线程安全;若做不到,应让配置显式失败,而不是静默改变语义。测试层次与接口合同可参考自动化测试、接口设计、风格与文档

常见陷阱

可移植性故障通常不是“编译器太旧”这一种原因,而是把某层观察错误地推广成另一层保证。审查时用下表把失败假设替换为可重复的证据。

失败假设为什么失败可移植替代
编译器产品版本足够新,所以一定支持 C23产品版本不等于当前翻译模式,语言与库支持也可能分阶段检查 __STDC_VERSION__,再对实际接口做编译/运行探测
一个 __STDC_NO_*__ 未定义,所以全部条件能力可用三个条件宏彼此独立分别读取宏,并分别测试原子、线程和 VLA 路径
本机是 LP64,所以整数、指针和外部格式都固定C 不规定项目采用 LP64,其他目标可能是 LLP64 或其他模型从标准头、sizeof 和实现 ABI 取值,外部格式显式编码
结构可直接逐字节比较或发送字节序、填充、对齐和对象表示可能不同逐成员比较,按协议逐字段序列化
能包含扩展头就说明它是标准接口实现扩展不会因常见而变成 ISO C 保证隔离平台适配层并提供标准后备或明确目标约束
严格编译无警告就证明程序可移植诊断不能证明所有运行期前置条件和 ABI 假设组合标准审查、实现文档、静态诊断和多目标行为测试

最后追问三件事:这条结论属于标准保证、实现记录还是本次探测?换一个翻译模式会不会变化?缺失时程序是降级、禁用还是拒绝构建?三问都能明确回答,能力声明才可进入支持矩阵。