【问题标题】:Why does printf print one extra byte of a character array, but only if the last element isn't the null character \0?为什么 printf 打印一个字符数组的一个额外字节,但前提是最后一个元素不是空字符 \0?
【发布时间】:2021-09-19 13:04:49
【问题描述】:

我正在学习 C,来自 Python,我试图理解 printf() 为何会这样。

我的理解是C中的字符串是一个字符数组,后面跟着一个空字符。

如果你这样声明你的字符串:

char string[] = "I am a string";
printf("char: %lu bytes\n", sizeof(char));
printf("%s\n", string);
printf("%lu\n", sizeof(string));

编译器会在末尾隐式插入空字符。输出:

char: 1 bytes
I am a string
14

注意一个 char 的大小是 1 个字节,字符串中有 13 个字符(10 个字母和 3 个空格),字符串的大小是 14 个字节。额外的字节是空字符。

或者,我们这样声明我们的字符串:

char string2[] = {'h', 'e', 'l', 'l', 'o', '\0'};
printf("%s\n", string2);
printf("%lu\n", sizeof(string2));

我们手动将空字符插入到我们的数组中,当我们打印它或获取它的大小时它的行为与预期一样:

hello
6

但是,如果我们不手动插入空字符,并尝试打印它:

char string3[] = {'w', 'o', 'r', 'l', 'd'};
printf("%s\n", string3);
printf("%lu\n", sizeof(string3));

我们得到(或者至少,在我的机器上,我得到了)

worldhello
5

它在最后打印 string2。我们可以深入挖掘一下,看看为什么:

char *strAddr = string2;
char *str3Addr = string3;
char *lastElemStr3 = &string3[4];

printf("Address of hello string:\t\t\t%p\n", strAddr);
printf("Address of world string:\t\t\t%p\n", str3Addr);
printf("Address of string 3 last element + one byte:\t\t%p\n", lastElemStr3 + 1);

给了

Address of hello string: 0x7ffee10768a4
Address of world string: 0x7ffee107689f
Address of string 3 last element + one byte: 0x7ffee10768a4

所以 printf 将我们的数组“超出”了一个字节并打印了该内存单元中的任何内容 - 在这种情况下,它恰好是存储前一个数组的位置。为什么这样做,只有在最后没有空字符的情况下?这有什么好处?

【问题讨论】:

  • 您正在使用两种不同类型的初始化程序。为什么您希望它们表现相同?
  • OT:“请注意 char 的大小是 1 字节”...sizeof(char) 始终为 1。
  • printf 读取超出数组边界,因为不包含终止空字符,因此您会引发未定义的行为。
  • @4386427 你能详细说明一下吗?
  • @HoldenNelson 来自 C 规范:“当 sizeof 应用于类型为 char [...] 的操作数时,结果为 1。”

标签: arrays c string printf


【解决方案1】:

printf 查找 NUL 字符以确定字符串的结尾。在您的情况下,因为它在string2 的末尾找到\0,在您的情况下恰好是内存中的下一个(您的堆栈包含string3string2string2,按此顺序) .

想象一下,没有一个字符串会有\0 字符 - printf 会读取当前函数堆栈之外的内容,可能会吸入其他变量、字符串、密码等......

它可能会崩溃,因为行为未定义。但是如果你不走运,可能会发生奇怪的事情,这将很难调试。至少,这是您程序中的一个漏洞。

【讨论】:

  • 实际上这是未定义的行为,因为读取超出了数组范围。
  • 所以你是说printf 只是吐出字符直到找到一个空字符?它不关心初始字符串中有多少元素或类似的东西?
  • 不,它没有。您只是传递一个指针,它基本上是printf 的起始地址。简而言之,从printf 的角度来看,您在内存中看到的是worldhello\0I am a string\0...someotherdata...。它只会读取直到找到第一个 \0 字符。在您的情况下,您很幸运在实际字符串之后有一个以 NUL 结尾的字符串,但通常可能有 anything (例如,您之前存储的一些密码)。这就是为什么你在这里真的需要小心。
  • '最有可能' - 崩溃可能是 UB 的结果,但有 至少 一样多在这种情况下它不会发生并且程序会以不可预知的行为继续运行(后者的检测要重得多,因此崩溃实际上意味着开发人员的运气......)。
  • @andreee 从超出堆栈边界的堆栈读取不是堆栈溢出。堆栈溢出是指当您完全填满堆栈时,例如使用永无止境的递归,或者当您的局部变量占用的空间超过堆栈上的可用空间时。
【解决方案2】:

为什么这样做,只有在最后没有空字符的情况下?这有什么好处?

您似乎知道 C 中的字符串是字符数组。

此外,您需要了解 C 中的数组在运行时的任何地方都没有存储数组大小。您可以在编译时使用sizeof 获取数组的大小,但是一旦编译了程序,就无法获取有关数组大小的信息。

而且...当您将数组传递给函数时(例如在调用printf 时),传递的不是数组,而是指向数组第一个元素的指针。这也强调了被调用函数无法知道数组大小。

这就引出了一个问题:printf接收到一个char数组时如何知道要打印的字符数?

这是通过使用特殊字符(空字符)作为标记来完成的,这意味着“字符串在此处结束”。因此printf 将继续打印字符,直到它看到空字符。

这样做的缺点是你可以调用printf 使用一个 包含空字符的字符数组。如果你这样做,printf 将不知道何时停止打印字符,迟早它会访问数组边界之外的内存。这种情况有未定义的行为,即 C 标准没有描述会发生什么。在 C 中,确保这种情况永远不会发生是您的响应能力。

另一方面,这样做有几个好处。例如:

  • 数组大小不必与字符串长度 (+1) 匹配,即您可以将长度为 3 的字符串存储在大小为 10 的数组中。

  • 您不需要内存来保存“大小”字段。这也意味着您不需要在运行时更新大小字段。此外,字符串长度没有上限。

简而言之,您可以说:C 风格的字符串约定在资源(内存、许多情况下的性能)方面具有优势,但它也允许您做一些真正的坏事。

为了好玩,人们常说:

  • C 让你可以轻松地射中自己的脚。

使用不带空字符元素的 char 数组作为字符串只是您如何“打自己的脚”的众多示例之一。

【讨论】:

  • 这一切现在都说得通了——非常感谢。
【解决方案3】:

为什么这样做,只有在最后没有空字符的情况下? 这有什么好处?

其他答案已经解释了正在发生的事情的机制。但是您必须记住的另一件事是您违反了规则。而当你违反规则时,几乎任何事情都可能发生

在这种情况下,如您所知,规则是 C 中的字符串是一个以\0 结尾的字符数组。以\0 结尾的not 字符数组不是正确的字符串。 printf 期望得到正确的字符串。 (嗯,实际上,它希望得到一个指向正确字符串的第一个元素的指针。)如果您传递 printf 一个不正确的字符串,任何事情都可能发生

这有什么好处?对你,用户来说,几乎没有。这是undefined behavior,你不应该依赖它。 (你不能依赖它,因为你无法预测它会做什么。下周它可能会做一些完全不同的事情。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-07-05
    • 1970-01-01
    • 1970-01-01
    • 2013-09-28
    • 1970-01-01
    • 2021-01-27
    • 1970-01-01
    相关资源
    最近更新 更多