18.3 功能特性测试宏(Feature Test Macros)
C 标准定义了一系列预定义宏,用于指示实现所支持的 C 语言版本、环境类型以及可选特性的支持情况。这些宏由编译器自动定义,无需包含头文件,常用于 #if、#ifdef 等条件编译指令,使代码能够适应不同的标准版本和实现。
1. 核心预定义宏
__STDC__
值:通常 1(表示实现意图符合 ISO C 标准,可能包含扩展)。__STDC_VERSION__
值(长整型常量):- C90/C95:未定义或 199409L
- C99:199901L
- C11:201112L
- C17:201710L
- C23:202311L(实现支持 C23 时至少此值)
用于检查支持的 C 标准版本。
__STDC_HOSTED__
值:1表示宿主实现(提供完整的标准库,程序通常运行在操作系统上);0表示独立实现 (freestanding implementation),例如裸机嵌入式环境,只提供标准库的一部分。__STDC_MB_MIGHT_NEQ_WC__
值:1(表示多字节字符的编码可能与宽字符不同)。
2. 可选特性宏(C11 及以后)
如果实现不支持相应可选特性,则定义这些宏为 1:
__STDC_NO_ATOMICS__:不支持原子操作(<stdatomic.h>)__STDC_NO_COMPLEX__:不支持复数类型(_Complex)__STDC_NO_THREADS__:不支持线程(<threads.h>)__STDC_NO_VLA__:不支持可变长度数组(VLA)
3. 其他相关宏
__STDC_ISO_10646__:表示wchar_t值遵循 ISO/IEC 10646(Unicode)特定版本(如 201112L 表示 2011 年 12 月版)。__STDC_ANALYZABLE__、__STDC_LIB_EXT1__等(较少用,涉及界限检查函数等扩展)。
实现还可能定义编译器特定宏(如 __GNUC__、__clang__、_MSC_VER),但这些非标准。
4. 用户定义特征测试宏
用户可在编译命令行(-D)或代码开头(包含任何头文件之前)定义某些宏,以请求特定扩展行为。这些宏由 POSIX 或具体的 C 库规定,不属于 ISO C 标准:
_POSIX_C_SOURCE:请求 POSIX 特性(值如 200809L)。_XOPEN_SOURCE:请求 X/Open 或 UNIX 特性。_GNU_SOURCE:请求 GNU 扩展。_ISOC11_SOURCE等(较少用)。
推荐优先检查 __STDC_VERSION__ 来适配标准特性。
5. 使用示例
#include <stdio.h>
int main(void) {
#ifdef __STDC__
printf("符合 ISO C 标准\n");
#endif
#if defined(__STDC_VERSION__)
printf("C 标准版本: %ld\n", __STDC_VERSION__);
#endif
#if __STDC_VERSION__ >= 202311L
printf("支持 C23 特性\n");
#elif __STDC_VERSION__ >= 201710L
printf("支持 C17 特性\n");
#else
printf("较旧 C 标准\n");
#endif
#if __STDC_HOSTED__ == 1
printf("宿主环境\n");
#else
printf("独立环境\n");
#endif
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
可能的输出:
符合 ISO C 标准
C 标准版本: 202311
支持 C23 特性
宿主环境
6. 注意事项
- 这些宏是常量表达式,可用于
#if预处理。 - 在编写跨版本代码时,优先使用
__STDC_VERSION__检查新特性(如 C23 的typeof、constexpr等)。 - 实际值取决于编译器和编译选项(例如用
-std=c23启用 C23 模式)。
7. 习题
7.1 巩固
正文第 5 节的示例只在一个标准模式下运行过。把它分别编译成五个版本:
gcc -std=c90 -Wall -Wextra ft.c -o ft90
gcc -std=c99 -Wall -Wextra ft.c -o ft99
gcc -std=c11 -Wall -Wextra ft.c -o ft11
gcc -std=c17 -Wall -Wextra ft.c -o ft17
gcc -std=c2x -Wall -Wextra ft.c -o ft2x2
3
4
5
要求:
- 把五次输出列成表,标明每个模式下哪几行出现、
__STDC_VERSION__的值各是多少。 -std=c90下示例能编译,但#if __STDC_VERSION__ >= 202311L这一行用到了未定义的宏。加上-Wundef再编一次,抄下两条诊断;再加-Werror,记录结果。- 把
#if __STDC_VERSION__ >= 202311L改写成#ifdef __STDC_VERSION__ >= 202311L,在-std=c17下编译。抄下诊断,说明预处理把它理解成了什么,以及为什么这个写法的行为与作者的本意无关。 -std=c2x下输出的是「支持 C17 特性」而不是「支持 C23 特性」。查gcc -std=c2x -dM -E - < /dev/null | grep __STDC_VERSION__,解释这一行输出与__STDC_VERSION__取值的关系。GCC 13.2 不认识-std=c23,把它报的错也记下来。
验收:五行的表格正确;第 2 问能看到两条 is not defined, evaluates to 0 的诊断;第 3 问的诊断是 extra tokens at end of #ifdef directive,并且程序进入了 C23 分支。
下面这段能力探测每一条都有毛病:
#include <stdio.h>
#ifdef __STDC_VERSION__ >= 201112L
#define HAVE_C11 1
#else
#define HAVE_C11 0
#endif
#if __STDC_NO_VLA__
#define HAVE_VLA 0
#else
#define HAVE_VLA 1
#endif
#ifdef __STDC_HOSTED__
#define IS_HOSTED 1
#else
#define IS_HOSTED 0
#endif
int main(void) {
printf("C11=%d VLA=%d hosted=%d\n", HAVE_C11, HAVE_VLA, IS_HOSTED);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
要求:
- 用
-std=c90、-std=c99、-std=c11、-std=c17各编译运行一次,把输出与诊断列成表。 HAVE_C11在-std=c99下已经是 1。说明#ifdef后面跟比较表达式时,预处理实际检查的是什么,为什么结果与作者的意图无关。HAVE_VLA在这台机器上恒为 1。加上-D__STDC_NO_VLA__=1再跑一次,确认它变成 0,说明这条探测本身写对了一半,错在哪一半。IS_HOSTED在-ffreestanding下仍然是 1。改成按值判断,再跑一次,确认为 0。- 修正全部三处,要求
-std=c90到-std=c17各做两次(带与不带-ffreestanding),全部加-Wundef -Werror,共八次编译零警告零错误,且IS_HOSTED随-ffreestanding正确变化。
验收:修正版在 -std=c90 下输出 C11=0 VLA=1 hosted=1,在 -std=c11 下输出 C11=1 VLA=1 hosted=1;-D__STDC_NO_VLA__=1 时 VLA=0;-ffreestanding 时 hosted=0。
7.2 编程
正文第 4 节列了 _POSIX_C_SOURCE、_XOPEN_SOURCE、_GNU_SOURCE 等「用户定义特征测试宏」,并说它们「不属于 ISO C 标准」。这一节的分量要靠一张可见性矩阵才测得出来。
要求:
- 在 Linux 上做一张 6×6 的表:行是
strdup、strtok_r、fileno、getline、posix_memalign、clock_gettime六个函数,列是不加宏、-D_POSIX_C_SOURCE=199309L、=200112L、=200809L、-D_XOPEN_SOURCE=700、-D_GNU_SOURCE。每格用-fsyntax-only编译一个只引用该函数名的main,记录声明可见或不可见。
/* 每格编译这样一个小文件,只换函数名与命令行 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
int main(void) {
(void)strtok_r;
return 0;
}2
3
4
5
6
7
8
9
10
11
- 表里会出现三种门槛:
strtok_r与clock_gettime在最低的199309L就可见,posix_memalign要到200112L,strdup与getline要到200809L。把这张表按门槛从低到高重排,说明同一个<string.h>里的strdup与strtok_r为什么要求不同的版本号。宏的值表示什么? - 用同一个测试在 64 位 Windows 的 MinGW-w64 GCC 13.2 上重跑。
strdup、strtok_r、fileno、clock_gettime无论加不加宏都可见,getline与posix_memalign无论加不加宏都不可见。据此说明:这些宏由谁解释、MinGW 的头文件为什么无视它们。 - 把宏的位置改一次:
late.c把#define _POSIX_C_SOURCE 200809L放在#include <string.h>之后,early.c放在最前面,都调用strdup。在 Linux 上各编译一次,记录差异并解释原因。再在 MinGW 上跑一次,说明为什么这里看不出差别。 - 回答:正文说这些宏「不属于 ISO C 标准」,可它们又确实能改变编译结果。它们由编译器还是 C 库定义?用
-std=c17与-std=gnu17下__STRICT_ANSI__是否定义、以及<string.h>里的#if __USE_XOPEN2K8一类条件来回答。
验收:Linux 上的表格里三档门槛与上面描述一致;Windows 上 strdup、strtok_r、fileno、clock_gettime 四行全为「可见」,getline、posix_memalign 两行全为「不可见」;第 4 问 early.c 通过、late.c 报 implicit declaration of function 'strdup'。
写一个 compat.h,把四处 C11/C23 特性包成一套可移植的宏,让同一份源码从 C99 编到 C23:
COMPAT_NULL /* 空指针常量 */
COMPAT_ALIGNOF(type) /* 对齐要求,结果是 size_t */
COMPAT_STATIC_ASSERT(tag, cond, msg) /* 编译期断言;tag 用于 C11 之前的实现 */
COMPAT_TYPEOF(x) /* 与操作数同类型的类型名 */2
3
4
要求:
- 每处特性用一套
#if分支:C11 及以上用标准写法,更早的版本退回编译器扩展(__typeof__、__alignof__、typedef char ...[cond ? 1 : -1])。不要用__STDC_VERSION__ >= 202311L判断 C23,理由见正文第 6 节。 COMPAT_STATIC_ASSERT在 C11 之前不能重复名字,所以宏带一个tag参数拼进typedef的名字里。用它写两条断言,验证sizeof(int) >= 2与CHAR_BIT >= 8。- 用
COMPAT_TYPEOF写一个SWAP(a, b)宏,交换任意同类型的两个对象,实参只求值一次。用它交换两个int、两个double、两个指针,各打印一次结果。 - 在
-std=c99、-std=c11、-std=c17、-std=c2x上加-pedantic -Wall -Wextra -Werror各编译一次。四次的诊断各是什么?如果有哪一次被-pedantic挡下,指出是SWAP的哪一步触发的。 - 回答:
COMPAT_TYPEOF在 C23 分支用typeof、在旧模式用__typeof__,这两者在 GCC 下行为一致。既然一致,为什么还要分两支?
验收:-std=c99 到 -std=c2x 都能编过并运行,SWAP 三组交换的输出分别是 2 1、2.5 1.5、8 7。
提示:_Static_assert 的 tag 参数在 C11 分支用不到,写成 COMPAT_STATIC_ASSERT(tag, cond, msg) _Static_assert(cond, msg) 即可,未使用的参数不会引起警告。SWAP 想做到「只求值一次」,需要一个临时对象;如果写成语句表达式 ({ ... }),-pedantic 会报 ISO C forbids braced-groups within expressions,这正是第 4 问要观察的点。
7.3 综合与挑战
下面是一个保护头文件的三道闸门,一道比一道严:
/* gate.h */
#ifndef GATE_H
#define GATE_H
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L
#error "this module requires C11 or later"
#endif
#if !defined(__STDC_HOSTED__) || __STDC_HOSTED__ != 1
#error "this module requires a hosted implementation"
#endif
#if defined(__STDC_NO_ATOMICS__)
#error "this module requires <stdatomic.h> support"
#endif
#endif2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(1) [3.2] 写一个 use.c 包含 gate.h,用五条命令编译,记录每次是编译通过还是停在哪一道闸门:-std=c11、-std=c17、-std=c99、-std=c17 -ffreestanding、-std=c17 -D__STDC_NO_ATOMICS__=1。再把 use.c 的 #include "gate.h" 挪到 #include <stdio.h> 之后,重跑前两条,说明结果有没有变化、原因是什么。
(2) [4.3] 把第一道闸门的条件反过来写成 #if __STDC_VERSION__ < 201112L || !defined(__STDC_VERSION__),在 -std=c90 -Wundef 下编译,抄下诊断。解释 || 的短路求值为什么救不了这个写法。
(3) [5.4] 第三个闸门只写了 #if defined(__STDC_NO_ATOMICS__)。先用 -D__STDC_NO_ATOMICS__=1 与 -D__STDC_NO_ATOMICS__=0 各编译一次,会发现两者都触发 #error。说明 defined 与「宏的值为真」的区别,并把它改成按值判断。接着补上第二重证据:闸门只信任宏,可宏没定义并不等于头文件能用。用 __has_include(<stdatomic.h>) 增加一道检查,并说明两道检查各能发现什么问题、各漏掉什么。最后在 -std=c11 上用 -D__STDC_NO_ATOMICS__=1 但保留 <stdatomic.h> 的情况跑一次,确认第二道检查不会误报。
(4) [6.4*] 把 gate.h 扩展成一个能力查询接口:除了三道编译期闸门,再提供一个 const char *gate_std_name(void),返回 "C11"、"C17"、"C23" 或 "unknown",用 __STDC_VERSION__ 分档。注意 GCC 13.2 在 -std=c2x 下报 202000L 而 C23 的标准值是 202311L,分档不能只按标准值写死。写测试在 -std=c11、-std=c17、-std=c2x 下各跑一次,输出 gate_std_name() 与 GATE_STD 两个值。本机三次输出依次是 C11 11、C17 17、C23 20,第三个数字说明版本号与标准值之间差了一截。
把 11816 的 compat.h 扩成一个真正能在两套环境上工作的可移植模块,并把它用到一个具体的接口上:
/* vec.h */
#include <stddef.h>
typedef struct vec vec;
vec *vec_new(size_t capacity); /* 失败返回 NULL */
void vec_free(vec *v);
int vec_push(vec *v, double value); /* 满了返回 -1,成功返回 0 */
size_t vec_size(const vec *v);
double vec_get(const vec *v, size_t index);2
3
4
5
6
7
8
9
要求:
vec.c里用malloc/realloc实现,容量增长按 2 倍。用COMPAT_STATIC_ASSERT断言sizeof(double) == 8与sizeof(vec) >= sizeof(size_t) * 2,用COMPAT_ALIGNOF确认double的对齐不超过max_align_t。- 在
-std=c99、-std=c11、-std=c17与-std=c2x上各编译一次,全部零警告;每套配置下都跑同一个测试:容量给 2,压入 10 个值(用来触发两次扩容),打印size、第 0 个与第 9 个元素,再用vec_get逐个核对后释放。 - 再把
vec.c拿到-ffreestanding -c下编一次。它会失败还是通过?用nm -u看它引用了什么,结合 18.1 关于独立实现的讨论,说明为什么「用-ffreestanding编过」不等于「能在单片机上跑」。 - 把 11816 的
COMPAT_NULL用在vec_new的失败返回上,并在 C99 下用-Wpedantic编译,看((void *)0)这个展开在返回vec *时有没有诊断。如果没有,说明标准对此的宽容之处在哪里。 - 用
-Werror把 1~4 步中的全部构建串成一条 shell 命令或一个Makefile,作为这个模块的回归测试。
验收:四套标准模式下测试输出一致;第 3 步的 nm -u 里能看到 malloc、realloc、free;一串构建命令在任意一套环境上执行,退出状态为 0。