跳到主要内容

数值转换、数学、排序、查找、随机与时间

本节目标

查询 C23 数值转换、数学、排序查找、随机数与时间接口。

C 的这些接口看似分散,其实都要求先确定合同:文本是否被完整消费、数值是否落在目标范围、数据是否按同一关系排序,以及测量的究竟是哪一种时间。本章从任务出发选择接口,把可复验计算与依赖实现、环境或当前时刻的结果明确分开。

文本数值转换与结束位置

<stdlib.h>strtolstrtoulstrtod 同时返回转换值,并通过 endptr 告诉调用方转换停在哪里。可靠检查不能只看返回值,因为合法输入本来就可能得到 0LONG_MAX 或无穷值。调用前先把 errno 清零;调用后依次检查是否消费了字符、剩余字符是否符合输入协议,以及接口是否用 ERANGE 报告范围错误。errno 的一般使用边界见错误码与错误宏

errno = 0;
char *end = NULL;
long value = strtol(text, &end, 10);

if (end == text) {
/* 没有消费任何字符 */
} else if (*end != '\0') {
/* end 指向协议不允许的剩余字符 */
} else if (errno == ERANGE) {
/* 数学结果超出 long 的可表示范围 */
} else {
/* value 可用 */
}

这里先清零不是为了把任何非零 errno 都解释为本次失败,而是为了在 strto* 规定的范围错误上下文中排除陈旧值。若协议允许尾随空白、单位或分隔符,就按协议继续检查 endptr,不要机械要求它立即指向 \0strtoul 还接受可选符号,因此“只要调用无错误就是非负文本”并不成立。

atoiatolatoll 不提供与 strto* 等价的错误诊断能力:调用方拿不到结束位置,也无法可靠地区分失败、合法零值和范围问题。处理外部输入或需要报告错误时,优先选择带 endptr 的接口,再显式检查目标业务范围。

整数工具与检查算术

<stdlib.h> 提供 abslabsllabsdivldivlldiv。这些接口并不会绕开整数类型的可表示范围:例如对相应有符号类型的最小值求绝对值时,正值无法表示;有符号最小值除以 -1 也不能得到可表示商。应在调用前排除这种输入,而不是在调用后观察一个已经越界的结果。

C23 将 <stdckdint.h> 列为标准头;它不属于 6.10.9.3 所列的条件特性。该头提供 ckd_addckd_subckd_mul 检查算术。宏根据结果对象的类型判断数学结果能否正确表示;返回非零表示不能正确表示,调用方必须先检查,再使用成功结果。

int total;
if (ckd_add(&total, left, right)) {
/* 处理不可表示的和 */
} else {
use(total);
}

检查算术不等于所有整数运算自动安全。没有经过这些宏的加减乘、除法、取模、移位、窄化转换以及参与长度计算的中间表达式仍各自遵守原有规则;即使最终写入 size_t,此前在较窄类型中发生的问题也不会被事后修复。对尚未完整实现 C23 的目标工具链,使用前仍应实际包含头文件并编译最小程序。最小编译探测检查的是目标工具链的 C23 实现覆盖度,不会把标准保证改写为可选能力。

实数数学、分类与错误报告

<math.h> 按功能提供平方根、幂、指数、对数、三角函数、舍入与余数等接口,并用 floatdoublelong double 变体表达精度族。每个函数都有自己的定义域和值域;实现是否支持 IEC 60559 语义,以及某个结果如何报告,都要分别确认。不能用“返回 NaN”作为统一失败规则:NaN 可能是合法传播的输入结果,某些错误产生无穷值或实现规定的值,而实现也不一定采用同一种报告机制。

fpclassifyisfiniteisinfisnanisnormal 用于分类浮点值,它们回答“结果属于哪一类”,不自动回答“调用为何失败”。math_errhandling 表明实现是否以 MATH_ERRNOMATH_ERREXCEPT 或两者报告数学错误;只有相应位存在时,才按具体函数合同检查 errno 或浮点异常标志。若检查 errno,仍应在调用前清零,并只解释该接口明确规定的错误。

errno = 0;
double result = log(input);
int category = fpclassify(result);

if ((math_errhandling & MATH_ERRNO) != 0 && errno != 0) {
/* 按 log 的合同解释 errno */
}

选择判断策略时,把三层问题分开:函数对输入的数学定义、结果的浮点分类,以及目标实现启用的错误报告能力。不要用精确相等普遍比较近似计算。多数 <math.h> 浮点函数的结果精度由实现定义;即使数学上的精确结果可以表示,也不等于标准保证函数必须返回该精确表示。只有核对并启用目标实现的 IEC 60559 绑定后,才能按相应绑定要求建立更强合同。

复数与类型泛化数学

<complex.h> 描述复数类型、I 以及 cabscrealcimagcexp 等复数运算;<tgmath.h> 提供按实参类型选择实数或复数数学接口的类型泛化宏。类型泛化能减少手工选择后缀,却不会改变被选中函数的定义域、精度或错误合同,也不应把可能具有多次求值风险的复杂表达式交给未经核对的实现扩展。

复数能力是条件特性,不能因为本机接受 <complex.h> 就断言所有 C23 实现都提供同样能力。移植时先检查实现文档和特性测试宏,例如实现是否定义 __STDC_NO_COMPLEX__;若项目依赖 IEC 60559 复数绑定,再检查 __STDC_IEC_60559_COMPLEX__,并以目标编译选项实际包含头文件、声明所需类型和编译最小探测程序。宏未声明某种绑定不等于基础浮点一定不可用,探测结论应限定到所需能力。

浮点环境、舍入与异常标志

<fenv.h> 让程序查询或尝试改变舍入方向,检测、清除或引发浮点异常,并用 fenv_t 保存和恢复浮点环境。浮点环境属于动态状态,一次 fesetroundfeclearexcept 会影响随后受该环境约束的运算,异常标志也可能由先前计算留下。因此先清除目标标志、执行受控计算,再查询规定的标志,不能把旧标志归因于当前表达式。

#pragma STDC FENV_ACCESS ON

if (feclearexcept(FE_ALL_EXCEPT) == 0) {
volatile double result = operation();
int raised = fetestexcept(FE_ALL_EXCEPT);
consume(result, raised);
}

FE_INVALIDFE_OVERFLOW 等单个 FE_* 异常宏只为实现支持的异常定义,而 FE_ALL_EXCEPT 表达该实现支持的异常集合。浮点环境与编译器优化、FENV_ACCESS 访问约定及实现对 IEC 60559 的绑定共同决定可观察行为。需要严格环境访问时,检查编译器是否支持 #pragma STDC FENV_ACCESS ON 及相关选项,再通过最小程序验证;需要 IEC 60559 语义时还要检查相应特性宏和实现文档。不能仅因 <fenv.h> 可包含,就推断所有异常宏、舍入方向和优化组合都可用。

排序、查找与比较器契约

<stdlib.h>qsort 接收数组首地址、元素数、单个元素大小和比较器。比较器把两个元素解释为正确类型,并按顺序关系返回负数、零或正数;调用方只能依赖符号,不能直接返回两个整数相减的结果,因为相减可能溢出。关系还必须一致:相等、先后顺序和传递性不能随调用变化。

static int compare_ints(const void *left_pointer, const void *right_pointer) {
int left = *(const int *)left_pointer;
int right = *(const int *)right_pointer;
return (left > right) - (left < right);
}

bsearch 的标准前置条件是数组相对当前键按“小于、等于、大于”分区;标准不要求每个分区内部也形成全局顺序。用兼容的全序比较器先执行 qsort 是常见且充分的做法,但不是规范写出的唯一布局。元素大小、数量与比较器解释仍必须和实际数组匹配。找不到时返回空指针;存在重复键时,找到重复键不保证返回哪一个副本。若业务需要第一个、最后一个或完整区间,应在命中后扩展边界,或使用明确实现该语义的查找算法。

下面的代表程序只使用固定输入:完整检查两次文本转换,再以同一可靠比较器排序和查找。代表程序不比较或打印 strtod 的浮点结果,也不调用 sqrtsqrt 留在以上按合同讨论的正文中。它不调用随机、当前时间、locale 格式化或依赖浮点环境变化的路径。

numeric_algorithm_report.c
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

static int compare_ints(const void *left_pointer, const void *right_pointer) {
int left = *(const int *)left_pointer;
int right = *(const int *)right_pointer;
return (left > right) - (left < right);
}

int main(void) {
const char *integer_text = "42";
char *integer_end = NULL;
errno = 0;
long integer = strtol(integer_text, &integer_end, 10);
if (errno != 0 || integer_end == integer_text || *integer_end != '\0') {
fputs("integer-conversion-error\n", stderr);
return EXIT_FAILURE;
}

const char *real_text = "12.5";
char *real_end = NULL;
errno = 0;
double real = strtod(real_text, &real_end);
if (errno != 0 || real_end == real_text || *real_end != '\0') {
fputs("real-conversion-error\n", stderr);
return EXIT_FAILURE;
}
(void)real;

int values[] = {19, -3, 12, 7};
size_t value_count = sizeof values / sizeof values[0];
qsort(values, value_count, sizeof values[0], compare_ints);

int key = 12;
const int *found = bsearch(
&key,
values,
value_count,
sizeof values[0],
compare_ints
);
if (found == NULL || *found != key) {
fputs("search-error\n", stderr);
return EXIT_FAILURE;
}

printf("integer=%ld\n", integer);
puts("real-conversion=complete");
printf("sorted=%d,%d,%d,%d\n", values[0], values[1], values[2], values[3]);
printf("found=%d\n", *found);
return EXIT_SUCCESS;
}

输出:

integer=42
real-conversion=complete
sorted=-3,7,12,19
found=12

伪随机数、种子与安全边界

rand 返回 0RAND_MAX 范围内的伪随机整数,srand 用无符号种子初始化其序列。没有先调用 srand 时,行为等同于先以种子 1 初始化;相同实现中重复使用同一种子可用于重放测试。这个接口的算法、质量和周期由实现决定,不保证不同实现产生相同序列,也不适合要求统计质量、不可预测性或并发独立状态的任务。

randsrand 不要求避免与其他伪随机序列函数调用之间的数据竞争。并发代码不能假定这组程序级序列内部同步;需要并发随机状态时,应选用合同明确、状态所有权清楚的其他接口或在外部建立同步。

rand 不是密码学随机源。密码、令牌、密钥和安全挑战值必须使用部署平台认可的密码学安全随机接口;ISO C23 标准库没有把 rand 提升为这种能力。rand() % n 还可能因 RAND_MAX + 1 不能被 n 整除而产生模偏差,不能把范围缩放和安全性混为一谈。

static int sample_with_explicit_seed(unsigned int seed) {
srand(seed);
return rand();
}

这段代码只展示种子与调用的合同。具体数值依赖实现和传入种子,因此不列出一个貌似跨平台固定的结果;程序级全局序列也意味着库函数或不同执行路径的额外调用会改变后续值。

日历时间与分解时间

<time.h>time_t 是能表示时间的实数类型。time_t 可表示时间的范围与精度由实现定义;time 返回值的编码则未指定。time 尝试取得当前日历时间。gmtimelocaltime 把日历时间转换为 struct tm 分解时间:前者使用 UTC 解释,后者使用本地时区规则。mktime 反向把按本地时间解释的 struct tm 规范化并转换为 time_t;不能把同一字段集合在 UTC 与本地语义之间直接互换。

struct tm 是字段化表示,不是持续时间类型。它包含年月日、时分秒、星期、年内日和夏令时提示等字段;月份与年份使用接口规定的偏移。时区变化、夏令时切换和实现可表示范围都可能让某些本地字段含糊、重复或无法转换。检查转换失败时按每个接口的返回合同处理,不要假设所有 time_t 值都能在所有实现中分解。

strftime 按当前 locale 把 struct tm 格式化进调用方缓冲区,返回写入的字符数;空间不足时返回零,而某些格式本来也可能产生空结果,所以协议应同时明确格式和容量。日历时间格式依赖 locale,本地时间依赖时区,当前时间还随运行时刻变化,这些内容都不适合作为固定教学结果。locale 的程序级影响见locale 类别与 setlocale

static int format_local_calendar_time(
char *buffer,
size_t capacity,
const time_t *calendar_time
) {
struct tm *parts = localtime(calendar_time);
if (parts == NULL) {
return 0;
}
return strftime(buffer, capacity, "%Y-%m-%d %H:%M:%S", parts) != 0;
}

处理器时间、时间基准与持续时间

先问要测什么,再选接口。clock 返回程序使用的处理器时间近似值,以 clock_t 表示;用 CLOCKS_PER_SEC 换算秒,并检查 (clock_t)-1。它不是墙上时间,也不承诺随人类等待等速前进,多线程程序中的累计方式还要看实现说明。

timespec_get 把选定时间基准的值写入 struct timespec,并返回实际使用的基准;只有 TIME_UTC 的定义是必需的。C23 还标准化了 timespec_getres,并为可选时间基准规定 TIME_MONOTONICTIME_ACTIVETIME_THREAD_ACTIVE;这三个基准宏若存在,timespec_gettimespec_getres 分别查询对应时间与分辨率,实现还可以提供其他 TIME_ 基准。调用方必须检查返回值,且不能把 TIME_UTC 自动当作单调时钟:墙上时间可能被校准。若需要持续时间测量,必须先说明时间基准是否满足单调性、分辨率和范围要求;名称相似的平台接口不自动成为 ISO C 保证。

difftime(end, beginning) 计算两个 time_t 日历时间之间以秒表示的差,避免调用方猜测 time_t 的具体算术表示。它本身不读取时钟,也不把本地 struct tm 字段差转换成可靠持续时间。墙上时间、处理器时间、日历时间和持续时间解决的是不同问题,不能混为一谈。

static int read_utc_base(struct timespec *stamp) {
return timespec_get(stamp, TIME_UTC) == TIME_UTC;
}

数值、算法、随机与时间陷阱

症状常见根因修正方向
非法文本被当作零只看 strto* 返回值检查 endptr 是否消费字符、剩余字符和范围错误
成功转换后仍报告错误读取到旧的 errno调用前清零,并仅按该接口规定解释
大整数排序偶发错序比较器用 left - right 而溢出用关系比较组合出负、零、正结果
bsearch 漏掉已有元素数组未按当前键形成所需分区,或排序与查找使用了不兼容的比较关系至少满足小于、等于、大于分区;通常先按同一全序比较器排序
测试换平台后随机序列改变rand 算法当成标准固定只在同一实现内重放,协议测试改用受控生成器
安全令牌可被猜测rand 当作安全熵源使用平台认可的密码学安全随机接口
相同时间字段转换不同忽略时区、夏令时或 locale明确 UTC、本地规则和格式化 locale
clock 测等待时间异常混淆处理器时间与墙上时间按任务选择并记录时间基准

还要避免把真正的条件、可选或实现相关能力悄悄写成最低保证:复数、IEC 60559 绑定、浮点环境访问和额外时间基准都应按各自的标准边界,再结合目标实现文档、特性宏及最小编译探测确认。探测到某项能力后,结论只覆盖相应工具链选项和部署目标。

数值、算法、随机与时间接口地图

头文件主要接口组适用任务与关键检查
<stdlib.h>strtolstrtoulstrtodabsdivqsortbsearchrandsrandRAND_MAX文本转换、整数工具、通用排序查找和基础伪随机;分别检查结束位置、输入边界、比较关系与用途限制
<errno.h>errnoERANGE仅在具体接口规定时解释错误码;可能读取前先清零
<stdckdint.h>ckd_addckd_subckd_mul检查加减乘的数学结果能否由结果类型正确表示
<math.h>sqrtlogfpclassifymath_errhandling实数数学、浮点分类和实现启用的错误报告
<complex.h>复数类型、Icabscrealcimag在目标实现提供复数能力时执行复数数学
<tgmath.h>类型泛化数学宏按实参类型选择对应的实数或复数接口
<fenv.h>fenv_tfegetroundfesetroundfeclearexceptfetestexcept在受支持的访问约定下查询和控制浮点环境
<time.h>time_tstruct tmmktimestrftimeclock_tclocktimespectimespec_gettimespec_getresdifftime日历时间、分解与格式化、处理器时间、指定基准取时与查询分辨率,以及时间差

反查时先选任务,再核对输入前置条件、失败返回和实现能力。数值转换与数学错误还可对照库合同与失败判断;涉及未定义行为、实现定义行为和可移植边界时,对照行为分类