scanf()、字段宽度、inf和nan

5
根据1999年的C标准,如果被实现支持,scanf()strtod()应该接受无穷大和NaN作为输入。这两个函数的描述语言很奇特,可能会有不同的解释。 scanf()的描述如下:

输入项被定义为最长的输入字符序列,它不超过任何指定的字段宽度,并且是匹配输入序列的前缀或匹配输入序列本身。

strtod()的描述如下:

主题序列被定义为输入字符串的最长初始子序列,从第一个非空格字符开始,具有预期的形式。

尽管后一段摘录似乎要求特定的"INF"、 "INFINITY"、 "NAN"或者"NAN(n-char-sequence-opt)"格式,但前一段并不要求这样,因此人们认为以下代码应该产生无穷大和NaN,因为字段宽度包含匹配输入序列的前缀:
int r;
double d;
d = 0; r = sscanf("inf", "%2le", &d);
printf("%d %e\n", r, d);
d = 0; r = sscanf("nan", "%2le", &d);
printf("%d %e\n", r, d);

还有关于scanf()的这一段:

a,e,f,g匹配一个可选符号的浮点数、无穷大或NaN,其格式与strtod函数的主题序列期望的格式相同。相应的参数应该是指向浮点数的指针。

这是否只是未能记录字段宽度为2(短于预期的最短形式“inf”或“nan”)不会使得与之匹配的前缀“in”和“na”有效的匹配而已?


@Shark,你好像不理解问题。 - Alexey Frunze
能解释一下为什么是-1吗? - Alexey Frunze
可能是我不太清楚,非常抱歉。 - Shark
2个回答

4
scanf 的行为规范中,“输入项”是指格式说明符处理期间所消耗的输入字符序列。在格式说明符处理完毕后,无论处理是否成功,流的位置都会精确地定位在输入项的最后一个字符之后,这一点在引用“输入项”定义后紧接着的句子中已经明确说明。

输入项后的第一个字符(如果有)仍未读取。

一旦读取了输入项,scanf 就会进行下一步操作(同一条款的第10段),其中整个输入项必须根据格式说明符进行转换:

10 …[输入项] 被转换为与转换说明符相适应的类型。如果输入项不是匹配序列,则指令执行失败:此条件称为匹配失败。

“匹配序列”在每个格式说明符的描述中都有定义;对于 f 说明符,它将是:

strtod 函数的主题序列期望相同。

正如引用中所述。
这与 strtod 所使用的算法不同。 strtod 找到最长可能的匹配序列,并且只要有一个(即使只有一个字符),它就会将其转换并将下一个字符的地址放置在提供的 endptr 参数中。
相比之下,scanf 必须处理输入流的限制,该限制不允许可靠地将读取指针倒回超过一个字符。 (请参见 ungetc 的定义。)因此,scanf 会读取,直到找到无法扩展匹配的字符,此时它会将该字符替换为输入流并尝试将已读取到的内容转换。与 strtod 不同的是,如果存在更短的有效序列,则它不能回溯到更短的有效序列。
strtod 的另一个区别是,scanf 可以被限制为最大长度,这对于转换未定界的定长输入字段很有用。对于 strtod,需要制作固定长度字段的 NUL 结尾副本,或者在适当的位置暂时插入 NUL 并稍后恢复被覆盖的字符。在这种情况下,重要的是验证 strtod 是否消耗了整个输入;如果没有,那么这将表示输入中存在垃圾。
两种输入,在使用scanfstrtod函数时可能会引起不同的输出结果。对于输入1E-@scanf会报告匹配失败,并且在随后的getchar中返回字符'@',而strtod将返回1.0,并使指针endptr指向字母E
指定格式长度也可能导致scanf返回匹配错误。例如,对于输入1E-7@scanf应用格式%2f%3f将失败,而%1f将转换成1.0%4f(或更大)将转换成.01,下一个指示符(或随后的输入函数)将留给字符@。如果%2f应用于infnan的输入,则会出现与输入1E-7相同的行为:吸收两个字符后匹配失败(因为截断的字段不是一个有效的浮点数)。
上述情况是否发生取决于C库的实现是否符合标准。Glibc不符合标准,并且在Linux平台上使用gcc或clang编译时,大多数情况下都会使用Glibc。这是因为clang没有捆绑标准C库,即使在libcxx项目中也没有。
我在Windows上进行的有限测试(使用在线编译器)表明,scanf的libcrt实现按照我的期望工作。我的FreeBSD库源代码检查表明,它的scanf将正确报告匹配失败,但可能会将读取光标回退超过一个字符。

给定输入1E-@scanf将报告匹配失败,随后的getchar将返回'@'。-- 不确定我是否理解你的意思;例如,使用sscanf("1E-@", "%le", &val);sscanf()将报告1个成功分配,val将保持值为1.0,并且输入流中的下一个字符是'@' - ad absurdum
@david: 我觉得这不是一个符合标准的库。如果我回到家,停电问题解决了,我会进行一些实验并做出更详细的回应。 - rici
我在gcc上尝试了这个代码,返回值为1val被赋值为1.0这里是Ideone链接,使用Clang编译的结果也相同。这只是一些支持性证据。scanf()函数似乎有一些不为人知的问题;直到今天我才注意到这个特别的问题。 - ad absurdum
1
@DavidBowling:Clang没有自己的libc,因此如果你在Linux上安装clang,你最终会使用glibc。而且glibc的scanf实现已知存在一些bug(例如,在输入“nope”的情况下,scanf("%lf",&v)无法ungetc o,以便p是输入流中的下一个字符)。FreeBSD的libc实现进行了多次ungetc,这是我上次尝试时的情况,因此它也可能产生非标准结果。我会看看是否能找到最近的BSD libc实现来测试,然后更新答案。(但我声称根据标准,答案是正确的。) - rici
1
@DavidBowling:一个相关的错误报告,包含经典的UlrichDrepperism:https://sourceware.org/bugzilla/show_bug.cgi?id=12701 - rici
1
我完全相信你是对的。在仔细查看标准相关部分后,我倾向于同意;标准在这里似乎非常清晰,但可能不是那么明显。感谢提供错误报告的链接。那似乎为我解决了问题(尽管Drepper先生的评论除外)。感谢讨论;我在这里学到了一些东西,如果我能再次投票,我会的 :) - ad absurdum

2

这个特定案例不需要进行记录,因为它已经在标准中的一条引文中覆盖:

输入项被定义为不超过任何指定字段宽度的最长输入字符序列,该序列是匹配的输入序列或前缀。

考虑这个简单的情况:

int ch;
int res;
const char *input_fmt;

ch = 0;
input_fmt = "%*d%c";
res = sscanf("123456789012345678901234567890abc", input_fmt, &ch);
printf("%d\t%c\n", res, ch);

// 1      a

由于%d转换说明符没有字段宽度,它将匹配无限多个字符0-9,这就是为什么在扫描完成后ch=='a'的原因;这里n的值并不重要。与使用input_fmt="%*2d%c"输出相比进行对比:
1      3

第一个转换规范的字段宽度限制为2个字符,导致%c匹配3。这就是“前缀”位的作用。12123456...的前缀,它将匹配%d转换规范。另一个例子:

input_fmt = "%*2d%c";
res = sscanf("1a3456789012345678901234567890bc", input_fmt, &ch);
printf("%d\t%c\n", res, ch);

// 1      a

字段宽度限制了与 %d 匹配的字符数为2,但匹配的输入序列长度为1,因为 a 不是十进制整数。

字段宽度限制匹配的输入是您的情况中 infnan 的原因:

double d;
int r;
char s[10];

// 0       0.000000        ""
*s = 0; d = 0; r = sscanf("infinity", "%2le%9s", &d, s);
printf("%d\t%f\t\"%s\"\n", r, d, s);

// 2       inf             "inity"
*s = 0; d = 0; r = sscanf("infinity", "%3le%9s", &d, s);
printf("%d\t%f\t\"%s\"\n", r, d, s);

// 0       0.000000        ""
*s = 0; d = 0; r = sscanf("-infinity", "%3le%9s", &d, s);
printf("%d\t%f\t\"%s\"\n", r, d, s);

// 2       -inf             "inity"
*s = 0; d = 0; r = sscanf("-infinity", "%4le%9s", &d, s);
printf("%d\t%f\t\"%s\"\n", r, d, s);

字段宽度限制了输入序列中的字符数,这可能会防止特殊形式(如-infnan)被识别。这种行为已经有文档记录,但也不容易完全理解。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接