解析数字时scanf()和strtol()/ strtod()之间的区别
注意:我完全重写了这个问题,以更恰当地反映出我为此设置的赏金。 请原谅与已经给出的答案有任何不一致之处。 我不想创建一个新问题,因为之前的答案可能会有所帮助。
我正在努力实现C标准库,并对标准的一个特定角落感到困惑。
该标准根据strtol
, strtoul
和strtod
的定义定义了scanf
函数系列接受的数字格式(%d,%i,%u,%o,%x)。
该标准还说fscanf()
只会将最多一个字符放回输入流中,因此strtol
, strtoul
和strtod
接受的某些序列对fscanf
是不可接受的(ISO / IEC 9899:1999,脚注251)。
我试图找到一些会表现出这种差异的价值观。 事实certificate,hex前缀“0x”,后跟一个不是hex数字的字符,就是两个函数系列不同的情况。
有趣的是,很明显没有两个可用的C库似乎在输出上达成一致。 (请参阅本问题末尾的测试程序和示例输出。)
我想听到的是解析“0xz”时会被视为符合标准的行为? 。 理想情况下引用标准中的相关部分来说明问题。
#include #include #include int main() { int i, count, rc; unsigned u; char * endptr = NULL; char culprit[] = "0xz"; /* File I/O to assert fscanf == sscanf */ FILE * fh = fopen( "testfile", "w+" ); fprintf( fh, "%s", culprit ); rewind( fh ); /* fscanf base 16 */ u = -1; count = -1; rc = fscanf( fh, "%x%n", &u, &count ); printf( "fscanf: Returned %d, result %2d, consumed %d\n", rc, u, count ); rewind( fh ); /* strtoul base 16 */ u = strtoul( culprit, &endptr, 16 ); printf( "strtoul: result %2d, consumed %d\n", u, endptr - culprit ); puts( "" ); /* fscanf base 0 */ i = -1; count = -1; rc = fscanf( fh, "%i%n", &i, &count ); printf( "fscanf: Returned %d, result %2d, consumed %d\n", rc, i, count ); rewind( fh ); /* strtol base 0 */ i = strtol( culprit, &endptr, 0 ); printf( "strtoul: result %2d, consumed %d\n", i, endptr - culprit ); fclose( fh ); return 0; } /* newlib 1.14 fscanf: Returned 1, result 0, consumed 1 strtoul: result 0, consumed 0 fscanf: Returned 1, result 0, consumed 1 strtoul: result 0, consumed 0 */ /* glibc-2.8 fscanf: Returned 1, result 0, consumed 2 strtoul: result 0, consumed 1 fscanf: Returned 1, result 0, consumed 2 strtoul: result 0, consumed 1 */ /* Microsoft MSVC fscanf: Returned 0, result -1, consumed -1 strtoul: result 0, consumed 0 fscanf: Returned 0, result 0, consumed -1 strtoul: result 0, consumed 0 */ /* IBM AIX fscanf: Returned 0, result -1, consumed -1 strtoul: result 0, consumed 1 fscanf: Returned 0, result 0, consumed -1 strtoul: result 0, consumed 1 */
与comp.std.c上的PL22.11(ANSI“C”)副主席Fred J. Tydeman的沟通揭示了这一点:
fscanf
输入项被定义为输入字符的最长序列,它是匹配输入序列的前缀,或者是匹配输入序列的前缀。 (7.19.6.2 P9)
这使得“0x”成为匹配输入序列的前缀的最长序列。 (即使使用%i
转换,因为hex“0x”是比十进制“0”更长的序列。)
输入项目之后的第一个字符(如果有)仍未读取。 (7.19.6.2 P9)
这使fscanf
读取“z”,并将其作为不匹配(遵守脚注251的单字符后推限制))。
如果输入项不是匹配序列,则指令的执行失败:此条件是匹配失败。 (7.19.6.2 P10)
这使得“0x”无法匹配,即fscanf
不应分配任何值,返回零(如果%x
或%i
是第一个转换说明符),并将“z”保留为输入流中的第一个未读字符。
strtol
strtol
(和strtoul
)的定义在一个关键点上有所不同:
主题序列被定义为输入字符串的最长初始子序列,从第一个非空白字符开始, 即预期forms 。 (7.20.1.4 P4,强调我的)
这意味着strtol
应该寻找最长的有效序列,在本例中为“0”。 它应该将endptr
指向“x”,并返回零作为结果。
我不相信解析可以产生不同的结果。 Plaugher引用只是指出strtol()
实现可能是一个不同的,更高效的版本,因为它可以完全访问整个字符串。
根据C99规范, scanf()
系列函数以与strto*()
函数族相同的方式解析整数。 例如,对于转换说明符x
它读取:
匹配可选的带符号hex整数,其格式与
strtoul
函数的主题序列的预期相同,其base
值参数值为16。
因此,如果sscanf()
和strtoul()
给出不同的结果,则libc实现不符合。
您的示例代码的预期结果应该是什么有点不清楚,但是:
如果base
为16
,则strtoul()
接受可选前缀0x
或0X
,并且规范读取
主题序列被定义为输入字符串的最长初始子序列,从第一个非空白字符开始,即预期forms。
对于字符串"0xz"
,在我看来,期望forms的最长初始子序列是"0"
,因此该值应为0
并且endptr
参数应设置为x
。
mingw-gcc 4.4.0不同意并且无法使用strtoul()
和sscanf()
解析字符串。 推理可能是预期forms的最长初始子序列是"0x"
– 这不是有效的整数文字,因此不进行解析。
我认为对标准的这种解释是错误的:预期forms的子序列应该总是产生一个有效的整数值(如果超出范围,则返回MIN
/ MAX
值并将errno
设置为ERANGE
)。
cygwin-gcc 3.4.4(据我所知使用newlib)如果使用了strtoul()
,也不会解析文字,但是根据我对sscanf()
的标准解释来解析字符串。
请注意,我对标准的解释倾向于你的初始问题,即标准只保证能够ungetc()
一次。 要确定0x
是否是文字的一部分,您必须提前读取两个字符: x
和后面的字符。 如果它不是hex字符,则必须将其推回。 如果有更多的标记要解析,你可以缓冲它们并解决这个问题,但如果它是最后一个标记,你必须ungetc()
这两个字符。
如果ungetc()
失败,我不确定fscanf()
应该做什么。 也许只是设置流的错误指示器?
总结解析数字时根据标准应该发生的事情:
- 如果
fscanf()
成功,结果必须与通过strto*()
获得的结果相同 -
与
strto*()
,fscanf()
失败了输入字符的最长序列,它是匹配输入序列的前缀,或者是匹配输入序列的前缀
根据
fscanf()
的定义不是最长的初始子序列[…]具有预期的forms
根据
strto*()
的定义
这有点难看,但是要求fscanf()
应该是贪婪但不能推回多个字符的必然结果。
一些库实现者选择了不同的行为。 在我看来
- 让
strto*()
无法使结果保持一致是愚蠢的( 糟糕的mingw ) - 推回多个字符,因此
fscanf()
接受strto*()
fscanf()
接受的所有值都违反了标准,但是有理由( 如果他们没有使用strto*()
话,请为newlibstrto*()
🙁 ) - 没有推回不匹配的角色,但仍然只解析“预期forms”的角色似乎是可疑的,因为角色消失在空气中( 坏glibc )
我不确定我是否理解这个问题,但有一件事,scanf()应该处理EOF。 scanf()和strtol()是不同种类的野兽。 也许你应该比较strtol()和sscanf()?
我不确定如何实现scanf()可能与ungetc()有关。 scanf()可以用完流缓冲区中的所有字节。 ungetc()只是将一个字节推送到缓冲区的末尾,并且偏移量也会改变。
scanf("%d", &x); ungetc('9', stdin); scanf("%d", &y); printf("%d, %d\n", x, y);
如果输入为“100”,则输出为“100,9”。 我没有看到scanf()和ungetc()如何相互干扰。 对不起,如果我添加了一个天真的评论。
对于scanf()函数和strtol()函数的输入,在Sec。 7.20.1.4 P7表示: 如果主题序列为空或没有预期的forms,则不进行转换; 如果endptr不是空指针,则nptr的值存储在endptr指向的对象中 。 此外,您必须考虑解析那些根据Sec规则定义的令牌的规则。 6.4.4常量 , 第二节中指出的规则。 7.20.1.4 P5 。
其余行为(例如errno值)应该是特定于实现的。 例如,在我的FreeBSD盒子里,我得到了EINVAL和ERANGE值,在Linux下也是如此,标准引用只有ERANGE errno值。
在重写问题后回答过时。 虽然评论中有一些有趣的链接。
如有疑问,请写一个测试。 – 谚语
在测试了我能想到的转换说明符和输入变量的所有组合之后,我可以说这两个函数族没有给出相同的结果是正确的。 (至少在glibc中,这是我可用于测试的。)
当三种情况相遇时会出现差异:
- 您使用
"%i"
或"%x"
(允许hex输入)。 - 输入包含(可选)
"0x"
hex前缀。 - hex前缀后面没有有效的hex数字。
示例代码:
#include #include int main() { char * string = "0xz"; unsigned u; int count; char c; char * endptr; sscanf( string, "%x%n%c", &i, &count, &c ); printf( "Value: %d - Consumed: %d - Next char: %c - (sscanf())\n", u, count, c ); i = strtoul( string, &endptr, 16 ); printf( "Value: %d - Consumed: %td - Next char: %c - (strtoul())\n", u, ( endptr - string ), *endptr ); return 0; }
输出:
Value: 0 - Consumed: 1 - Next char: x - (sscanf()) Value: 0 - Consumed: 0 - Next char: 0 - (strtoul())
这让我很困惑。 显然sscanf()
不会在'x'
处挽救,或者它无法解析任何 "0x"
前缀hex。 所以它读了'z'
并发现它不匹配。 但它决定只使用前导"0"
作为值。 这意味着将'z'
和 'x'
推回去。 (是的,我知道我在这里用于简单测试的sscanf()
不能在流上运行,但我强烈认为它们使得所有...scanf()
函数的行为完全一致。)
所以… one-char ungetc()
并不是真正的原因,这里……?: – /
是的, 结果不同 。 我仍然无法正确解释,但…… 🙁