18. 标准库的使用契约
C 标准库 (C standard library) 是 C 语言标准的组成部分,由一组标准头文件中声明的函数、宏和类型组成,内容涵盖输入输出、字符串处理、数学运算、动态内存管理、日期与时间、错误处理、数值转换、排序与查找、泛型数学函数和多线程等方面。
本章的完整目录:
- 18.1 翻译环境与执行环境:独立实现与宿主实现的区别。
- 18.2 标准库中的宏:描述实现能力和类型范围的宏。
- 18.3 功能特性测试宏:判断语言版本和实现能力。
1. 头文件
1.1 <stdio.h>
提供输入输出功能,包括:
- 文件操作(如
fopen(),fclose(),fread(),fwrite()) - 格式化输入输出(如
printf(),scanf(),fprintf(),fscanf()) - 字符和字符串的输入输出(如
getchar(),putchar(),fgets(),puts()) - 缓冲区控制(如
fflush(),setvbuf()) - 文件定位(如
fseek(),ftell(),rewind())
等。
1.2 <stdlib.h>
提供通用工具函数,包括:
- 动态内存分配(如
malloc(),calloc(),realloc(),free()) - 随机数生成(如
rand(),srand()) - 整数算术(如
abs(),div(),labs()) - 搜索和排序(如
qsort(),bsearch()) - 环境交互(如
system(),getenv(),exit()) - 字符串转换(如
atoi(),atof(),strtol())
等。
1.3 <string.h>
提供字符串操作函数,用于:
- 字符串复制和连接(如
strcpy(),strncpy(),strcat(),strncat()) - 字符串比较(如
strcmp(),strncmp(),strcoll()) - 字符串搜索(如
strchr(),strstr(),strspn(),strpbrk()) - 内存操作(如
memcpy(),memmove(),memset(),memcmp()) - 字符串长度计算(如
strlen())
等。
1.4 <math.h>
提供数学函数,包括:
- 三角函数(如
sin(),cos(),tan(),asin()) - 双曲函数(如
sinh(),cosh(),tanh()) - 指数和对数(如
exp(),log(),log10()) - 幂运算(如
pow(),sqrt()) - 取整函数(如
ceil(),floor(),round()) - 绝对值和余数(如
fabs(),fmod())
等。
1.5 <time.h>
提供时间和日期功能:
- 时间测量(如
time(),clock(),difftime()) - 时间转换(如
localtime(),gmtime(),mktime()) - 格式化时间显示(如
strftime(),asctime(),ctime())
1.6 <stdint.h>
提供固定宽度整数类型和相关宏:
- 精确宽度整数类型(如
int8_t,int16_t,int32_t,int64_t) - 最小宽度类型(如
int_least8_t,int_least16_t) - 最快最小宽度类型(如
int_fast8_t,int_fast16_t) - 指针整数类型(如
intptr_t,uintptr_t) - 最大宽度整数类型(如
intmax_t,uintmax_t) - 范围宏(如
INT8_MIN,INT8_MAX,UINT8_MAX)
等。
1.7 <ctype.h>
提供字符分类和转换函数:
- 字符分类(如
isalpha(),isdigit(),isspace(),ispunct()) - 字符转换(如
toupper(),tolower()) - 区域设置相关的字符处理
等。
1.8 <assert.h>
提供断言功能:
- 运行时断言检查(
assert()宏) - 编译时静态断言(
static_assert)
1.9 <signal.h>
提供信号处理功能:
- 信号处理函数注册(
signal()) - 信号发送(
raise())
1.10 <locale.h>
提供本地化支持:
- 区域设置控制(
setlocale()) - 本地化数据访问(
localeconv())
1.11 <threads.h>
提供多线程支持(C11 起):
线程管理:
thrd_create()- 创建新线程thrd_join()- 等待线程结束thrd_detach()- 分离线程thrd_current()- 获取当前线程thrd_exit()- 终止当前线程
互斥量:
mtx_init()- 初始化互斥量mtx_lock()- 加锁mtx_unlock()- 解锁mtx_destroy()- 销毁互斥量
条件变量:
cnd_init()- 初始化条件变量cnd_signal()- 唤醒一个等待线程cnd_broadcast()- 唤醒所有等待线程cnd_wait()- 等待条件变量cnd_destroy()- 销毁条件变量
线程局部存储:
tss_create()- 创建线程特定存储tss_get()- 获取线程特定值tss_set()- 设置线程特定值tss_delete()- 删除线程特定存储
1.12 <stdatomic.h>
提供原子操作支持(C11 起):
- 原子类型定义(如
atomic_int,atomic_flag) - 原子操作函数(如
atomic_store(),atomic_load(),atomic_exchange(),atomic_compare_exchange_strong())
1.13 <complex.h>
提供复数运算支持:
- 复数类型定义(
complex) - 复数运算函数
- 复数三角函数
- 复数指数和对数函数
1.14 <tgmath.h>
提供泛型数学函数支持:
- 包含
<math.h>和<complex.h> - 为其中的数学函数提供泛型宏,按实参的类型自动选用对应的函数
1.15 <stdbit.h>
提供位操作函数和宏(C23 起):
- 前导与尾随位计数:
stdc_leading_zeros(),stdc_leading_ones(),stdc_trailing_zeros(),stdc_trailing_ones() - 首个 0/1 位的位置:
stdc_first_leading_zero(),stdc_first_leading_one(),stdc_first_trailing_zero(),stdc_first_trailing_one() - 位计数:
stdc_count_zeros(),stdc_count_ones() - 2 的幂与位宽:
stdc_has_single_bit(),stdc_bit_width(),stdc_bit_floor(),stdc_bit_ceil() - 字节序:
__STDC_ENDIAN_LITTLE__,__STDC_ENDIAN_BIG__,__STDC_ENDIAN_NATIVE__
1.16 <stdckdint.h>
提供带溢出检查的整数运算(C23 起):
- 检查算术(宏):
ckd_add,ckd_sub,ckd_mul
注意
- 使用标准库函数前,应包含声明它的头文件。
- 应检查函数的返回值,了解函数对实参的要求,避免未定义行为。
- 标准库已提供的功能,应优先使用标准库函数,而不是自行实现。
2. 独立实现和宿主实现
C 语言的实现分为两种:独立实现 (freestanding implementation) 和宿主实现 (hosted implementation)。
2.1 独立实现
独立实现的执行环境通常没有操作系统支持,常见于单片机程序、实时操作系统 (RTOS) 内核、设备驱动程序、操作系统内核和引导加载程序。
在独立环境中,只保证提供以下头文件:
<float.h><iso646.h><limits.h><stdalign.h><stdarg.h><stdbool.h><stddef.h><stdint.h><stdnoreturn.h>
这些头文件提供了基本的类型定义、常量和宏,但不包含任何需要操作系统支持的功能。
2.2 宿主实现
宿主实现的执行环境通常是操作系统,桌面应用程序、命令行工具和服务器程序都运行在这种环境中。宿主实现提供全部 C 标准库头文件,包括文件操作、标准输入输出和动态内存管理等功能。
3. 习题
下面这段程序在 GCC 13.2 下用 -std=c17 -Wall -Wextra 编译,能通过,也能运行:
#include <stdio.h>
int main(void) {
char *p = malloc(32);
if (p == NULL) {
return 1;
}
strcpy(p, "hello");
printf("%s %.3f %c\n", p, sqrt(2.0), toupper('a'));
free(p);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
- 上机编译,把全部
-Wimplicit-function-declaration相关的诊断抄下来,指出缺了哪几个头文件,程序中哪一处用到了它们。 - 补上头文件,确认警告全部消失、输出不变。
- 解释
malloc缺少声明时为什么危险:编译器按什么类型理解它的返回值?在返回指针宽度与int不同的平台上会发生什么? - 同一份程序改用
-fno-builtin再编译运行,记下结果(本机直接崩溃)。再分别给malloc、strcpy补上正确的头文件、其余保留,用两次实验判断崩溃主要来自哪一处。说明 GCC 的内建函数知识在这里起了什么作用,以及为什么不能靠它保证程序正确。
写一个“头文件能力清单”程序:不包含被测头文件,而是用 __has_include 逐个判断它们是否存在,输出一张表。
要求:
- 判断这些头文件:
<stdio.h>、<stdlib.h>、<string.h>、<math.h>、<time.h>、<stdint.h>、<ctype.h>、<assert.h>、<signal.h>、<locale.h>、<threads.h>、<stdatomic.h>、<complex.h>、<tgmath.h>、<stdbit.h>、<stdckdint.h>。 - 第一列是头文件名(含尖括号,左对齐),第二列是
yes或no。 - 先尝试把
__has_include(<stdio.h>)写进一个普通宏再在main里调用,记下编译器的报错,说明__has_include只能出现在什么位置。再写一个#define HAS(name) __has_include(name)的宏,在#if里调用它,确认这种间接写法可用。 - 整理成每个头文件一条
#if的写法后重新编译运行,把本机结果与 18.1 和 18.3 中说的标准配置对照,指出哪些头文件缺失、对应的可选特性宏是否同时被定义。
本机 MinGW-w64 GCC 13.2 上 <threads.h>、<stdbit.h>、<stdckdint.h> 都不存在。按标准,实现不支持 <threads.h> 时应当定义宏 __STDC_NO_THREADS__,本机却查不到它。把这个观察写进结论。
写一个模块 minimal.c,它必须能作为独立实现的一部分被编译:只包含 <stddef.h>、<stdint.h>、<stdbool.h>,不使用任何输入输出、动态内存和字符串函数。
接口如下:
size_t bytes_copy(uint8_t *restrict dst, const uint8_t *restrict src, size_t count);
bool bytes_equal(const uint8_t *a, const uint8_t *b, size_t count);
size_t u32_to_dec(uint32_t value, char *out, size_t size);
bool is_little_endian(void);2
3
4
要求:
- 各函数自己用循环实现,不调用库函数。
u32_to_dec把value写成十进制文本并补上终止空字符,返回写出的字符数;空间不足时返回 0 且不改动缓冲区。- 用
gcc -std=c17 -ffreestanding -Wall -Wextra -c minimal.c编译,要求零警告,再用nm -u minimal.o确认没有未定义符号。 - 另写一个宿主程序调用这四个函数并打印结果,验证
4294967295能正确转换、长度不足时返回 0、bytes_copy与bytes_equal结果正确。 - 如果把
printf换进minimal.c里,-ffreestanding下编译还过得去,但nm -u会多出什么?据此说明独立实现为什么不能依赖标准输入输出。
下面这个模块把标准库的活重新实现了一遍,而且实现有缺陷:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static size_t mdr_length(const char *s) {
size_t n = 0;
while (s[n] != '\0') {
n++;
}
return n;
}
static void mdr_copy(char *dst, const char *src) {
size_t n = mdr_length(src);
for (size_t i = 0; i <= n; i++) {
dst[i] = src[i];
}
}
static int compare_int(const void *a, const void *b) {
return *(const int *)a - *(const int *)b;
}
static void report_sorted(int *values, size_t count) {
qsort(values, count, sizeof values[0], compare_int);
for (size_t i = 0; i < count; i++) {
printf("%d ", values[i]);
}
printf("\n");
}
int main(void) {
int values[] = {-2000000000, 2000000000, 0};
report_sorted(values, 3);
char text[32] = "abcdef";
mdr_copy(text + 2, text);
printf("[%s]\n", text);
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
28
29
30
31
32
33
34
35
36
37
38
39
40
本机运行输出(第一行末尾有一个空格,来自循环里的 printf("%d ", ...)):
2000000000 -2000000000 0
[ababababa]2
- 指出两处真正的缺陷:比较函数里的减法溢出、
mdr_copy在源与目标重叠时的行为。再说明第 3 处看似可疑的写法(把数组末尾的终止空字符一并复制)为什么本身没有问题。 - 用标准库改写:排序用
qsort加一个不会溢出的比较函数;复制换成能处理重叠区的函数。给出修正后的完整程序与输出。 mdr_length与mdr_copy都在重复<string.h>已有的功能。用一句话说明为什么这里应当直接调用标准函数,而不是自己写。- 把
report_sorted的sizeof values[0]改成sizeof values。说明qsort这时会按什么步长在数组里移动、越过数组末尾多远,为什么运行结果不可预料,以及编译器为什么发现不了这个错误。