c语言 - 为什么可以在 scanf 的转换说明符中嵌入空字符



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

#include <stdio.h>
int
main(void)
{
char buf[32] = "";
int x;
x = scanf("%31[^]", buf);
printf("x = %d, buf=%s", x, buf);
}
$ printf 'foonbar' | ./a.out
x = 1, buf=foo

由于字符串文字"%31[^]"包含嵌入的 null,因此似乎应该将其视为与"%31[^"相同的处理,并且编译器应该抱怨[不匹配。 事实上,如果你替换字符串文字,clang 会给出:

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

为什么在传递给 scanf 的字符串文本中嵌入空字符是有效的?

--编辑--

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

首先,Clang在这里完全无法输出任何有意义的诊断,而GCC确切地知道发生了什么 - 所以再次GCC 1 - 0 Clang。

至于格式字符串 - 好吧,它不起作用。要scanf的格式参数是一个字符串。字符串以终止 null 结束,即您提供给scanf的格式字符串是

scanf("%31[^", buf);

在我的计算机上,编译程序给出了

% gcc scanf.c
scanf.c: In function ‘main’:
scanf.c:8:20: warning: no closing ‘]’ for ‘%[’ format [-Wformat=]
8 |     x = scanf("%31[^]", buf);
|                    ^
scanf.c:8:21: warning: embedded ‘’ in format [-Wformat-contains-nul]
8 |     x = scanf("%31[^]", buf);
|                     ^~

扫描集必须具有右右括号],否则转换说明符无效。如果转换说明符无效,则行为未定义。

而且,在我运行它的计算机上,

% printf 'foonbar' | ./a.out
x = 0, buf=

质检部

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

首先,C 中的字符串在定义上以第一个结束。 您始终可以对此规则嗤之以鼻,例如,通过编写一个中间带有显式的字符串文本。 但是,当您这样做时,后面的字符大多是不可见的。 很少有标准库函数能够看到它们,因为当然,几乎所有解释 C 字符串的东西都会在找到的第一个停止。

但是:您作为第一个参数传递给scanf的字符串通常会解析两次 - 我所说的"解析"是指实际上被解释为可能包含特殊%序列的scanf格式字符串。 它始终将在运行时通过 C 运行时库中scanf的实际副本进行分析。 但它通常也由编译器在编译时解析,以便编译器可以在 % 序列与调用它的实际参数不匹配时发出警告。 (当然,scanf的运行时库代码无法执行此检查。

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

而且,令我相当惊讶的是,看起来编译器中的scanf格式解析代码可以(在某些情况下确实)一些特殊和意想不到的事情。 Clang没有(它根本不抱怨格式错误的字符串),但gcc说"%["格式"和"格式中嵌入的"\0"都没有关闭']"。 所以它注意到了。

这是可能的(尽管仍然令人惊讶),因为编译器至少可以看到整个字符串文字,并且能够注意到空字符是程序员插入的显式字符,而不是编译器附加的更常见的隐式字符。 事实上,gcc发出的警告"在格式中嵌入'\0'"证明,至少gcc是相当肯定地为适应这种可能性而编写的。 (有关编译器"查看"整个字符串文字的能力的更多信息,请参阅下面的脚注。

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

至少,该代码无法知道是显式的,并且后面有"真实"字符。 该代码必须在它找到的第一个处停止。 所以它的操作就好像格式字符串是

"%31[^"

当然,这是一个格式不正确的字符串。 运行时库代码不需要执行任何合理的操作。 但是我的副本和你的副本一样,能够读取完整的字符串"foo"。 这是怎么回事?

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

我通过尝试变体对此进行了测试

x = scanf("%31[^o]", buf);

这也匹配并打印了"foo",而不是"f"。

当然,显然事情并不能保证像这样工作。 @AnttiHaapala已经发布了一个答案,显示他的C RTL拒绝使用畸形的扫描字符串扫描"foo"。


脚注: 大多数时候,嵌入字符串中的确实过早地结束了它。 大多数情况下,后面的所有内容实际上都是不可见的,因为在运行时,每一段字符串解释代码都将在它找到的第一个停止,无法知道它是程序员显式插入的还是编译器隐式附加的。 但正如我们所看到的,编译器可以分辨出区别,因为编译器(显然)可以看到整个字符串文字,与程序员输入的完全一样。 这是证据:

char str1[] = "Hello, world!";
char str2[] = "Helloworld!";
printf("sizeof(str1) = %zu, strlen(str1) = %zun", sizeof(str1), strlen(str1));
printf("sizeof(str2) = %zu, strlen(str2) = %zun", sizeof(str2), strlen(str2));

通常,字符串文字上的sizeof会给您一个比strlen大的数字 1。 但是此代码打印:

sizeof(str1) = 14, strlen(str1) = 13
sizeof(str2) = 13, strlen(str2) = 5

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

char str3[5] = "Hello";

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

sizeof(str3) = 5, strlen(str3) = 10

我有点幸运。str3没有尾随,既不是我插入的,也不是编译器附加的,所以strlen从最后航行,可以很容易地数出数百或数千个字符,然后在内存中的某个地方找到一个随机,或者崩溃。

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

不能像"%31[^]"那样直接将空字符指定为扫描集的一部分,因为字符串的分析以第一个空字符结尾。

"%31[^]"scanf()解析,就好像它是"%31[^"一样。 因为它是无效的scanf()说明符,UB可能会跟随。 编译器可以提供比scanf()看到的更多的诊断。


空字符可以是扫描集的一部分,如"%31[^n]"。 这将读取除'n'以外的所有字符,包括空字符

在读取空字符的异常情况下,要确定读取扫描的字符数,请使用"%n"

int n = 0;
scanf("%31[^n]%n", buf, &n);
scanf("%*1[n]"); // Consume any 1 trailing n
if (n) {
printf("First part of buf=%s, %d characters read ", buf, n);
}

相关内容

  • 没有找到相关文章

最新更新