24. 标准版本、可选能力与扩展
C 标准中有一部分特性是可选的,实现可以不提供。使用这些特性之前,应先在编译期检查实现是否支持。
本节可跳过
这些内容在不同工具链上的支持度差异较大,且不少项目不会用到。若当前目标是先掌握主线语法,可以先跳过,后续按需回看。
本章的完整目录:
1. Annex K 与 __STDC_WANT_LIB_EXT1__
C11 引入了可选的边界检查接口族(常见为 *_s)。启用方式通常是在包含相关头文件前定义 __STDC_WANT_LIB_EXT1__ 为 1,然后再通过 __STDC_LIB_EXT1__ 判断实现是否真的提供了这组接口。
#define __STDC_WANT_LIB_EXT1__ 1
#include <string.h>
#if defined(__STDC_LIB_EXT1__)
/* 可使用 memcpy_s / strcpy_s 等接口 */
#endif2
3
4
5
6
2. 其他可选特性的检查
并发、原子等可选特性,实现不支持时会定义相应的宏,例如 __STDC_NO_THREADS__、__STDC_NO_ATOMICS__。用 #ifdef 检查这些宏,缺少的支持在编译期就能发现。
3. 习题
3.1 巩固
正文说「启用方式通常是在包含相关头文件前定义 __STDC_WANT_LIB_EXT1__ 为 1」,这个「之前」不是风格要求,而是预处理顺序决定的。
要求:
- 写一个程序,在
#include <string.h>之后才写#define __STDC_WANT_LIB_EXT1__ 1,然后用#if defined(__STDC_LIB_EXT1__)判断并向标准输出打印Annex K visible或Annex K not visible。 - 把
#define移到#include之前,再编译运行一次。 - 用
gcc -E预处理两次,比较输出里string.h展开后的内容有什么不同,说明为什么两次的宏判断结果相同或不同。 - 在报告里写明你这台机器上两次运行的结果。本机 MinGW-w64 GCC 13.2 与 Linux 上的 GCC 14.2 都输出
Annex K not visible,也就是两次一样。这不是程序写错了,而是两个实现都没有提供 Annex K。
验收:能说清「在包含头文件前定义」生效的原因,并解释「两次结果一样」与「两次结果不一样」分别对应什么实现。
写一个可移植的字符串复制函数,实现放在一个 .c 文件里,接口放在头文件里:
/* 成功返回 0;dstsize 为 0 或内容放不下时返回 -1。
dstsize 大于 0 时,无论成功还是失败,dst 都以 '\0' 结尾。 */
int copy_string(char *dst, size_t dstsize, const char *src);2
3
要求:
- 文件开头定义
__STDC_WANT_LIB_EXT1__为 1,并在包含<string.h>之后用#if defined(__STDC_LIB_EXT1__)决定走哪条路。 - 有 Annex K 时调用
strcpy_s,把它的返回值折算成 0 或 -1。 - 没有 Annex K 时手写循环复制,放不下时不写入内容,把
dst[0]置为'\0'并返回 -1。 dstsize为 0 时直接返回 -1,不能写dst。指针为空的检查也加上。- 用
gcc -std=c17 -Wall -Wextra编译,再用-D__STDC_LIB_EXT1__=201112L编译一次,看两条路径能否都编过。这一步的结果正好说明了正文那句话的分量:宏要由实现提供,用户自己定义它并不能变出接口。MinGW-w64 的头文件里始终声明了strcpy_s(它把 MSVC 的安全函数当作扩展提供),所以强制定义宏后能编过,运行时走的是strcpy_s;glibc 上没有strcpy_s,强制定义宏只会得到「隐式声明」的错误。把两次结果都记下来。
验收:下面四次调用的输出依次为 0 [hello]、-1 []、-1、0 []。
char buf[8];
printf("%d [%s]\n", copy_string(buf, sizeof buf, "hello"), buf);
printf("%d [%s]\n", copy_string(buf, sizeof buf, "12345678"), buf);
printf("%d\n", copy_string(buf, 0, "x"));
printf("%d [%s]\n", copy_string(buf, sizeof buf, ""), buf);2
3
4
5
3.2 编程
写一个 features.h,集中回答「这个工具链提供哪些可选能力」,再写一个 main.c 打印结果。
features.h 里定义这些宏:FEATURES_C11、FEATURES_C23、FEATURES_ATOMICS、FEATURES_THREADS、FEATURES_ANNEX_K,定义的条件分别是:
__STDC_VERSION__已定义且不小于201112L。__STDC_VERSION__已定义且不小于202000L。- 没有定义
__STDC_NO_ATOMICS__,且__has_include(<stdatomic.h>)为真。 - 没有定义
__STDC_NO_THREADS__,且__has_include(<threads.h>)为真。 - 定义了
__STDC_LIB_EXT1__。
__has_include 只在编译器支持时才写,用 #if defined(__has_include) 包住。
要求:
- 用
-std=c99、-std=c17、-std=c2x各编译一次,把五行结果做成表格。 - 再加一次
-D__STDC_NO_THREADS__=1 -D__STDC_NO_ATOMICS__=1,确认后两行随之改变。 - 观察 MinGW-w64 GCC 13.2:
-std=c17下threads.h不存在,但__STDC_NO_THREADS__并未定义。若只按正文的写法检查#ifdef __STDC_NO_THREADS__,程序会报告线程可用,直到#include <threads.h>时才在编译期失败。把这一点写进报告,并说明为什么第 4 条要同时检查两个条件。 - 把
__STDC_VERSION__的实测值也打印出来。GCC 14 用-std=c23(或-std=c2x)时报的是202000L而不是202311L,检查「版本号是否够 C23」会漏掉这类实现。
验收:在 MinGW-w64 GCC 13.2 上,-std=c99 只打印 atomics: yes;-std=c17 打印 C11: yes、atomics: yes;-std=c2x 再加 C23: yes;两次都不打印 threads: yes。在提供 <threads.h> 的 glibc 上,-std=c17 与 -std=c2x 都打印 threads: yes。四次运行都不打印 Annex K: yes。
3.3 综合与挑战
写一个 portable_counter 模块(portable_counter.h 与 portable_counter.c),用同一份源码适配三种实现能力:
typedef struct portable_counter portable_counter;
portable_counter *pc_create(void);
void pc_destroy(portable_counter *c);
void pc_add(portable_counter *c, int n);
long pc_value(const portable_counter *c);
const char *pc_backend(void); /* 返回 "atomic"、"mutex" 或 "serial" */2
3
4
5
6
7
要求:
- 有原子能力时(没定义
__STDC_NO_ATOMICS__且__has_include(<stdatomic.h>)),内部用atomic_long,pc_add用atomic_fetch_add。 - 没有原子但有线程能力时,内部用
mtx_t加普通long,pc_add与pc_value都在锁内访问。 - 两者都没有时退化为普通
long,pc_backend返回"serial",并在头文件注释里写明此时不能多线程使用。 pc_destroy只销毁实际初始化过的资源。mtx_init失败时pc_create返回NULL,不能泄漏内存。- 测试程序用 4 个线程各调用
pc_add25000 次,最后打印backend与pc_value;没有线程能力时改为主线程循环 100000 次。
验收:
- 正常编译运行,输出
backend=atomic value=100000。 - 加
-D__STDC_NO_ATOMICS__=1,输出backend=mutex value=100000。 - 再加
-D__STDC_NO_THREADS__=1,输出backend=serial value=100000,且测试程序里不能出现<threads.h>的引用。 - 三种配置都用
-Wall -Wextra编过,没有警告。-fsanitize=thread在三者上都不必强求:glibc 上与 C11 线程库共用时常见DEADLYSIGNAL崩溃,这是 TSan 的已知限制,不是程序的问题。能测就测,不能测就在报告里写明原因,并改用pthread把同一段测试驱动重写一遍再测。 - 在报告里说明:为什么模块里用宏选择实现,而不用运行期的
if。提示:mtx_t、atomic_long这些类型在头文件里就决定了,运行期的分支无法让编译器在缺少<threads.h>时仍然通过。