本章导读

数字与文本的互相转换看似小事,实际是事故高发区:atoi 遇到非数字静默返回 0、strtoll"解析到哪算哪"留下半截垃圾、%.17g 输出冗长而 %.6f 丢精度、INT64_MIN 边界让"先转正再取反"的实现算错。XRT 的 number 模块用三个设计回答这些问题:严格解析(整段合法或整体失败,宽松度由标志显式开启)、最短往返(浮点输出位数恰好保证解析回原值)、格式串(类 Python 风格,分组/进制/百分比一次声明)。本章把三条线讲透,边界值测试的思想也随之展开——它是"第 6 章故障注入"在数值世界的镜像:都是主动制造极端条件,验证实现的小数据之外是否同样可靠。

26.1引入

三个事故现场。现场一:配置文件里端口写成 "8080x",atoi 返回 8080——尾部的 x 被静默丢弃,服务起了个"看起来对"的端口。现场二:日志里的浮点用 %.6f 打印,读回来精度丢了三个位,回归比对永远失败;改 %.17g 又变成 0.10000000000000001 这种没人想读的东西。现场三:解析 " -1_234.567_890e-2 "——首尾空白、数字内下划线分组、科学计数法三件套,标准库一个都不同意,自己写又是一个下午。

三个现场的共同根因:标准库的转换函数诞生于宽松时代,把"容错"做成了默认行为——而工程要的恰恰相反:默认严格、宽松显式。XRT 的解析函数不解析到哪算哪:输入视图整段合法才返回值,任何杂质都失败并在错误槽留下明确类别;要允许空白与下划线,用标志位显式声明。

26.2概念

严格解析:要么全对,要么失败

1输入视图整段必须构成一个合法数字
2宽松可选空白/下划线用解析标志显式允许,默认拒绝
3边界正确INT64_MIN 等边界值精确处理,无中间溢出
4失败诊断整体失败 + 错误槽记录,不留半截结果

xrtIntParse(视图, 基数, 标志, 出参) 的严格性是三层:首尾不容空白(除非 XNUMBER_PARSE_SPACE)、数字内不容下划线(除非 XNUMBER_PARSE_SEPARATOR)、整段消耗完毕才算成功——"8080x" 在第一层就出局。浮点版 xrtNumParse 同语义,加上对科学计数法、特殊值(inf/nan)的显式处理。边界值纪律值得单独强调:测试输入用 INT64_MIN——任何"先转正数再取反"的实现都会在这一步算错(正数 9223372036854775808 溢出),只有全程按负数累积的实现才能通过。把边界值写进测试是本章练习的硬要求。

最短往返:序列化浮点的事实标准

xrtNumString(值, 精度) 默认(精度 0)输出最短往返表示:十进制位数恰好保证 Parse 回来与原 double 逐位相等。它比 %.17g 短得多(0.1 就输出 0.1,不是 0.10000000000000001),又不像 %.6f 丢精度——序列化-反序列化闭环零误差。什么时候手动指定精度?展示给人看的时候(报表里两位小数);机器往返一律最短往返。

格式串:一次声明全部格式诉求

格式串效果等价标准库写法
,d千分位十进制:-123,456,789无(手写循环)
#_X0X 前缀+下划线分组+大写十六进制组合多个标志
,.2f千分位+两位小数定点无
.1%百分比(自动 ×100 加 %)手算再拼
c整数码点渲染为 UTF-8 字符wctomb 转换

xrtIntFormat / xrtUIntFormat / xrtNumFormat 三个入口覆盖有符号、无符号、浮点,产物全部拥有式(xrtFree 释放)。格式串风格类 Python 的 format 规范——写过 Python 的读者零学习成本,没写过的也比记住 printf 的 %08X 与 %'d(哪个平台支持哪个)省心。

与 printf 家族的分工

xrtFormat(第 2 章见过)与本章的 Format 系不是竞争:前者拼任意混合内容("订单 %s 共 %d 件"),后者专攻数字的展示格式(千分位、进制、百分比)。经验法则:文本模板用 Format,数字展示用本章 Format 系,两者都返回拥有式产物、都走 printf 风格的语法子集(拒绝 %n,第 4 章提过的安全纪律)。

26.3示例

完整程序:严格解析与多进制输出

来自仓库范例 examples/number/integer/main.c——边界值 INT64_MIN 的双重验证:

examples/number/integer/main.c源码 ↗



















 */

#include <stdio.h>

#include <xrt.h>



int main(void)
{
	int64 iValue;
	str sDecimal;
	str sHex;

	




	 */
	if ( !xrtIntParse(XRT_STR_LITERAL(" -9_223_372_036_854_775_808 "),
		10, (uint32)XNUMBER_PARSE_SPACE |
		(uint32)XNUMBER_PARSE_SEPARATOR, &iValue) ) {
		return 1;
	}

	/* 十进制原样输出;十六进制加 0X 前缀并大写。 */
	sDecimal = xrtIntString(iValue, 10, 0);
	sHex = xrtIntString(iValue, 16,
		(uint32)XNUMBER_PREFIX | (uint32)XNUMBER_UPPER);
	if ( (sDecimal == NULL) || (sHex == NULL) ) {
		xrtFree(sHex);
		xrtFree(sDecimal);
		return 1;
	}
	printf("%s\n%s\n", sDecimal, sHex);
	xrtFree(sHex);
	xrtFree(sDecimal);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/number/integer/main.c -lws2_32 -liphlpapi-9223372036854775808-0X8000000000000000

刚才发生了什么。 ① 解析输入是 INT64_MIN 的十进制文本——模块用两个标志显式允许了空白与下划线(示例输入里有),宽松度白纸黑字。② 边界正确性的实证:解析出 -9223372036854775808 并回显——如果实现是"先转正再取反",正数侧在 9223372036854775808 处溢出,结果差一,这个输出就是不合格的信号。③ xrtIntString 以 16 进制输出并加 0X 前缀与大写——输出格式由标志声明,不需要手拼前缀。把这个示例改坏是本章练习的一部分:删掉解析标志、输入带上杂质,观察严格失败而不是静默截断。

完整程序:格式化全家

来自 examples/number/format/main.c——五种格式串一次展示:

examples/number/format/main.c源码 ↗






















 */

#include <stdio.h>

#include <xrt.h>



int main(void)
{
	str sInteger = xrtIntFormat(
		INT64_C(-123456789), XRT_STR_LITERAL(",d"));
	str sHex = xrtUIntFormat(
		UINT64_C(0xDEADBEEF), XRT_STR_LITERAL("#_X"));
	str sFloat = xrtNumFormat(
		1234567.895, XRT_STR_LITERAL(",.2f"));
	str sPercent = xrtNumFormat(
		0.125, XRT_STR_LITERAL(".1%"));
	str sCharacter = xrtIntFormat(
		20320, XRT_STR_LITERAL("c"));

	/* 任何一个失败都统一走清理(xrtFree 允许 NULL)。 */
	if ( (sInteger == NULL) || (sHex == NULL) ||
		 (sFloat == NULL) || (sPercent == NULL) ||
		 (sCharacter == NULL) ) {
		xrtFree(sInteger);
		xrtFree(sHex);
		xrtFree(sFloat);
		xrtFree(sPercent);
		xrtFree(sCharacter);
		return 1;
	}
	printf("%s\n%s\n%s\n%s\n%s\n",
		sInteger, sHex, sFloat, sPercent, sCharacter);
	xrtFree(sInteger);
	xrtFree(sHex);
	xrtFree(sFloat);
	xrtFree(sPercent);
	xrtFree(sCharacter);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/number/format/main.c -lws2_32 -liphlpapi-123,456,7890XDEAD_BEEF1,234,567.9012.5%你

刚才发生了什么。 ① 千分位整数——报表、日志里的可读性标配,标准库要手写插逗号的循环,这里一个 ,d。② 十六进制加下划线分组——调试器风格的 0XDEAD_BEEF,前缀、分组、大写三个诉求一个格式串。③④ 定点与百分比——注意百分比是"自动 ×100 加 %",输入 0.125 输出 12.5%,省掉手算与手拼。⑤ 字符渲染:整数 20320 按 Unicode 码点渲染成 UTF-8 的"你"——码点值与字符的转换(第 27 章字符集家族的接口在这里先见一面)。五行输出全部是拥有式产物、逐个 xrtFree。

26.4契约

  • 严格默认:整段合法才成功;宽松(空白/下划线)用标志显式开启;失败设置错误、输出不写。
  • 边界精确:INT64_MIN/UINT64_MAX 等边界值解析精确;实现无"先转正"路径。
  • 最短往返:xrtNumString 默认精度即最短往返;机器序列化不手动指定精度。
  • 拥有式产物:String/Format 系返回 str,恰好一次 xrtFree。
  • 进制显式:解析基数、输出进制与前缀都由参数/标志声明,不猜不默认十六进制。

从示例到工程:解析函数的三个宿主

解析函数在工程里的三种挂法,决定了错误处理策略。入口校验:配置加载、协议字段解码在系统边界调用解析——失败即拒绝整条输入,错误带上"哪个字段、什么杂质"的上下文(错误格式化构造的用武之地,第 4 章)。热路径转换:每请求都跑的数字提取——严格性换来的是"失败即 bug"的确定性,热路径不需要兜底分支。展示层格式化:Format 系面向人眼(日志、报表、错误消息),千分位与百分比在这里,机器间传输用最短往返而不是格式串。三宿主对应"边界拒绝、路径断言、展示格式"三种姿态——用错姿态比用错函数更常见。

一个测试观:边界值先于覆盖面

本章反复出现 INT64_MIN 不是巧合,它示范了一种测试观:边界值的选择比用例数量更重要。数字转换的边界清单:各类型最值、零与负零、前导零、单字符、空串、正负号单独出现、最大长度数字串——十五个精选边界能杀掉九成的实现缺陷,而一万个随机值未必碰到任何一个。这个观念在第 6 章(FailAfter 的注入点选择)与第 30 章(JSON 数值边界)会反复回收。

常见错误

坑 1:拿 atoi 的心态用严格解析

症状:从"一直能跑"的代码迁移到 XRT 解析后,同一批配置突然大量解析失败——错误集中在带空白或杂质的字段。

原因:标准库的宽松行为(跳过空白、解析到哪算哪)被当成了契约;严格解析把这些问题从"静默错误值"变成"显式失败"——失败不是回归,是原来就在的 bug 第一次被看见。

错误写法
int iPort = atoi(sConfig);   /* "8080x" → 8080,"x" 静默丢弃 */
正确写法
int64 iPort;
if ( !xrtIntParse((xstrview){ sConfig, strlen(sConfig) }, 10, 0, &iPort) ) {
	ReportConfigError(sConfig);   /* "8080x" 在这里失败——杂质被看见 */
	return false;
}
坑 2:浮点序列化手动指定精度

症状:序列化-反序列化闭环校验偶发失败;或日志里浮点变成 17 位的"精确噪音"。

原因:手动精度两头是坑——太短丢精度(往返不再相等),太长输出噪音(.17g 的宿命)。

错误写法
char sBuf[32];
snprintf(sBuf, sizeof(sBuf), "%.17g", fValue);   /* 0.1 → 0.10000000000000001 */
正确写法
str sText = xrtNumString(fValue, 0);   /* 最短往返:0.1 就是 0.1 */
/* ... 使用 sText ... */
xrtFree(sText);

编程练习

基础:严格性体检

对 "42"、" 42"、"4_2"、"42x"、"" 五个输入分别用默认标志与全开标志解析,记录十次结果——先笔算预期(哪个成功、值是多少、哪个失败),再运行对拍;对拍全对才进入下一题。

进阶:边界值测试集

给 xrtIntParse 写边界测试集:INT64_MIN/MAX、UINT64_MAX、-0、前导零、纯下划线、单空格——每个输入断言精确结果。提示:这是仓库测试的缩影,边界值选择比断言数量更重要。

挑战:配置读取器(三模块联动)

实现 read_int(配置文本, 键):按行 Split(第 25 章)、Cut 出键值、值走严格解析(允许空白不允许下划线)、键忽略大小写。验收标准:五种畸形输入(杂质、越界、空值、缺键、重复键)全部有区分度的错误报告;INT64_MIN/MAX 配置值往返精确。

小结

知识点速查
严格解析xrtIntParse/xrtNumParse:整段合法或失败;宽松用标志开
解析标志XNUMBER_PARSE_SPACE(空白)/ XNUMBER_PARSE_SEPARATOR(下划线)
整数输出xrtIntString:进制+前缀+大写由标志声明
最短往返xrtNumString(值, 0):位数恰好保证往返相等
格式串,d 千分位 / #_X 十六进制 / ,.2f 定点 / .1% 百分比 / c 码点
拥有式全部产物 str,恰好一次 xrtFree;Format 系也有 To 调用方缓冲变体
边界纪律测试必含 INT64_MIN——它专杀"先转正再取反"的实现
三宿主边界校验拒绝 / 热路径断言 / 展示层格式——姿态比函数更重要
测试观十五个精选边界胜过一万个随机值——选择先于覆盖

📖 本章涉及的 API 参考手册:number(22 个条目)

点击查看完整签名、参数约束、返回值与错误说明