为什么可以在 scanf 的转换说明符中嵌入空字符?

Wil*_*ell 9 c scanf

也许我误解了我的结果,但是:

#include <stdio.h>

int
main(void)
{
    char buf[32] = "";
    int x;
    x = scanf("%31[^\0]", buf);
    printf("x = %d, buf=%s", x, buf);
}
$ printf 'foo\n\0bar' | ./a.out
x = 1, buf=foo
Run Code Online (Sandbox Code Playgroud)

由于字符串文字"%31[^\0]"包含一个嵌入的空值,它似乎应该被视为与 相同"%31[^",并且编译器应该抱怨[是不匹配的。事实上,如果你替换字符串文字,clang 会给出:

warning: no closing ']' for '%[' in scanf format string [-Wformat]

为什么在传递给 scanf 的字符串文字中嵌入空字符会起作用?

- 编辑 -

以上是未定义的行为,只是碰巧“工作”。

Ste*_*mit 3

这是一个相当奇怪的情况。我认为有几件事正在发生。

\n

首先,C 中的字符串根据定义以第一个结尾\\0。您总是可以嘲笑这个规则,例如编写一个\\0中间带有显式符号的字符串文字。但是,当您这样做时, 后面的字符\\0大多是不可见的。很少有标准库函数能够看到它们,因为几乎所有解释 C 字符串的操作都会在\\0它找到的第一个地方停止。

\n

但是:作为第一个参数传递给的字符串scanf通常会被解析两次——“解析”的意思是实际上解释为可能包含特殊%序列的 scanf 格式字符串。它总是会在运行时由scanfC 运行时库中的实际副本进行解析。但它通常也会在编译时由编译器进行解析,以便如果 % 序列与您调用它的实际参数不匹配,编译器可以警告您。(当然, 的运行时库代码scanf无法执行此检查。)

\n

当然,现在这里存在一个非常重要的潜在问题:如果编译器执行的解析在某种程度上与实际执行的解析不同怎么办?scanf运行时库中实际代码执行的解析不同怎么办?这可能会导致令人困惑的结果。

\n

而且,令我相当惊讶的是,编译器中的 scanf 格式解析代码似乎可以(在某些情况下确实)做一些特殊和意想不到的事情。clang 没有(它根本不抱怨格式错误的字符串),但 gcc 说“没有关闭 \xe2\x80\x98]\xe2\x80\x99 for \xe2\x80\x98%[\xe2\ x80\x99 格式”和“嵌入 \xe2\x80\x98\\0\xe2\x80\x99 格式”。所以它正在注意。

\n

这是可能的(尽管仍然令人惊讶),因为编译器至少可以看到整个字符串文字,并且能够注意到空字符是程序员插入的显式字符,而不是由程序员附加的更常见的隐式字符。编译器。事实上,gcc 发出的警告“embedded \xe2\x80\x98\\0\xe2\x80\x99 in format”证明 gcc 至少是为了适应这种可能性而编写的。(有关编译器“查看”整个字符串文字的能力的更多信息,请参阅下面的脚注。)

\n

但第二个问题是,为什么它(似乎)在运行时起作用?scanfC 库中的实际代码在做什么?

\n

该代码至少无法知道 是\\0显式的以及后面有“真实”字符。该代码必须在\\0它找到的第一个代码处停止。所以它的操作就像格式字符串一样

\n
"%31[^"\n
Run Code Online (Sandbox Code Playgroud)\n

当然,这是一个格式错误的字符串。运行时库代码不需要做任何合理的事情。但我的副本,就像你的一样,能够读取完整的字符串“foo”。那是怎么回事?

\n

我的猜测是,在看到%[the 并^决定它将扫描与某些集合不匹配的字符之后,它实际上完全愿意推断丢失的字符]并继续从扫描集中匹配字符,最终得到没有排除的字符。

\n

我通过尝试变体来测试这一点

\n
    x = scanf("%31[^\\0o]", buf);\n
Run Code Online (Sandbox Code Playgroud)\n

这也匹配并打印“foo”,而不是“f”。

\n

当然,显然事情并不能保证像这样工作。@AnttiHaapala 已经发布了一个答案,表明他的 C RTL 根本拒绝使用格式错误的扫描字符串扫描“foo”。

\n
\n

脚注:\n大多数时候,嵌入到\\0字符串中确实会提前结束它。大多数时候,后面的所有内容\\0实际上都是不可见的,因为在运行时,每段字符串解释代码都会在\\0它找到的第一个地方停止,无法知道它是由程序员显式插入还是由编译器。但正如我们所见,编译器可以分辨出差异,因为编译器(显然)可以看到整个字符串文字,与程序员输入的完全一样。这是证明:

\n
char str1[] = "Hello, world!";\nchar str2[] = "Hello\\0world!";\n\nprintf("sizeof(str1) = %zu, strlen(str1) = %zu\\n", sizeof(str1), strlen(str1));\nprintf("sizeof(str2) = %zu, strlen(str2) = %zu\\n", sizeof(str2), strlen(str2));\n
Run Code Online (Sandbox Code Playgroud)\n

通常,sizeof在字符串文字上给出的数字比 1 大strlen。但这段代码打印:

\n
sizeof(str1) = 14, strlen(str1) = 13\nsizeof(str2) = 13, strlen(str2) = 5\n
Run Code Online (Sandbox Code Playgroud)\n

只是为了好玩我也尝试过:

\n
char str3[5] = "Hello";\n
Run Code Online (Sandbox Code Playgroud)\n

不过,这一次strlen给出了一个更大的数字:

\n
sizeof(str3) = 5, strlen(str3) = 10\n
Run Code Online (Sandbox Code Playgroud)\n

我有点幸运。 str3没有尾随\\0,既不是我插入的,也不是编译器附加的,所以strlen从末尾开始,并且可以轻松地计算出数百或数千个字符,然后\\0在内存中找到随机的某个位置,或者崩溃。

\n