【问题标题】:need for the last '\0' in fgets需要 fgets 中的最后一个 '\0'
【发布时间】:2013-08-28 19:17:24
【问题描述】:

我见过fgets(例如here)的几种用法,如下所示:

char buff[7]="";

(...)

fgets(buff, sizeof(buff), stdin);

有趣的是,如果我提供像“aaaaaaaaaaa”这样的长输入,fgets 将在此处将其截断为“aaaaaa”,因为第 7 个字符将用于存储 '\0'

但是,当这样做时:

int i=0;
for (i=0;i<7;i++)
{
    buff[i]='a';
}
printf("%s\n",buff);

我将永远得到 7 个'a's,并且程序不会崩溃。但是如果我尝试写 8 个'a's,它会的。

正如我后来看到的,这样做的原因是,至少在我的系统上,当我分配char buff[7](有或没有="")时,第8个字节(从1开始计数,而不是从0开始)得到设置为 0。据我猜测,事情就是这样完成的,这样一个带有 7 次写入的 for 循环,然后是一个格式化读取的字符串,可以成功,无论要写入的最后一个字符是否为 '\0',从而避免程序员在单独编写字符时自己设置最后一个'\0'。

由此可见

fgets(buff, sizeof(buff), stdin);

然后提供过长的输入,生成的buffstring 将自动包含两个'\0' 字符,一个在数组内,一个在数组后面,由系统写入。

我也观察到这样做

fgets(buff,(sizeof(buff)+17),stdin);

仍然可以工作,并输出一个很长的字符串,而不会崩溃。根据我的猜测,这是因为fgets 会一直写到sizeof(buff)+17,而最后一个要写入的字符将恰好是'\0',确保任何即将进行的字符串读取过程都会正确终止(尽管内存被弄乱了)无论如何都起来了)。

那么,fgets(buff, (sizeof(buff)+1),stdin); 呢?这将用完在buff 中正确分配的所有空间,然后在其后写入'\0',从而覆盖...之前由系统写入的'\0'。换句话说,是的,fgets 会越界,但可以证明,当写长度只加一个时,程序永远不会崩溃。

所以最后,问题来了:为什么fgets 总是以'\0' 终止其写入,而另一个'\0',由系统放置在数组之后,已经存在?为什么不逐一进行基于for-loop 的写入,它可以访问整个数组并写入程序员想要的任何东西,而不会危及任何东西?

非常感谢您的回答!

编辑:确实,没有证据可能,只要我不知道分配 buff[7] 时神秘出现的第 8 个'\0' 是否是 C 标准的一部分,特别是对于字符串数组.如果没有,那么......它只是运气好:-)

【问题讨论】:

  • 请仔细考虑,因为某些东西没有崩溃,这意味着它实际上是正确的;通常像这样的错误会导致“未定义的行为”。有时你会得到一个段错误,有时你不会。如果你有 buff[7],则不能保证第 8 个字节是 \0,它可以是任何东西。
  • “我总是会得到 7 个 'a's,并且程序不会崩溃” - 你期望它应该/可能崩溃至少表明你理解 未定义的行为。关于您的问题,因为这就是 fgets 的行为方式。如果您有char a; 并通过&amp;a 的任意大小大于1,您会期望 任何确定性 吗?它是一个 C-api,和大多数一样,它的行为要么有用,要么未定义,具体取决于 的调用方式。
  • 我了解在我的单机上进行的任何测试都无法证明任何事情。我只是将字符串视为一个数组,您可以在其中编写任何您想要的内容而不考虑最后一个单元格将包含什么(就像您在int[] 中一样),然后将其视为一个字符串,即作为一个词,对缺少的终止字符无所不在的恐惧。因此,标准可能已将第 8 个 '\0' 作为硬设置参数包含在内。因为我不知道标准的细节...我在问这个问题:这第八个'\0' 是 C 标准的一部分吗?
  • 你说:我总会得到7个'a',程序不会崩溃。但是,如果我尝试写 8 'a's,它会。 不幸的是,当你写 7 个 a's 时它不会崩溃,但你正在调用未定义的行为,并且很容易发生崩溃。一个接一个的错误是阴险的,因为它们会让你陷入一种虚假的安全感。您可能会发现在buff[6] 之后的堆栈上有一个未使用的字节(因为下一个变量需要在偶数边界上对齐)。但你不能依赖它......
  • @MrBrody 由于您使用的是 7 字节数组,因此它可能会被填充零。尝试使用一个 8 字节的数组,看看你是否很幸运。当您要求n 字节时,您将得到保证n 字节,但您可能得到更多。

标签: c string format fgets


【解决方案1】:

但是可以证明,当写的长度只加1时,程序永远不会crash。

不!你不能证明!不是在数学证明的意义上。您只展示了在您的系统上,使用您的编译器,使用您使用的那些特定编译器设置,使用特定环境配置,它可能不会崩溃。这远非数学证明!

事实上,C 标准本身,虽然它保证您可以获得“数组的最后一个元素之后的一个位置”的地址,但它也声明取消引用该地址(即尝试从该地址读取或写入)是未定义的行为

这意味着在这种情况下,实现可以做一切。它甚至可以用幼稚的推理来做你所期望的(即工作——但这​​纯粹是运气),但它也可能会崩溃,或者它也可能会格式化你的高清(如果你非常非常不走运的话)。在编写系统软件(例如设备驱动程序或在裸机上运行的程序)时尤其如此,即当没有操作系统可以保护您免受编写错误代码的最严重后果时!

编辑这应该回答评论中提出的问题(C99 标准草案):

7.19.7.2 fgets 函数

概要

#include <stdio.h>
char *fgets(char * restrict s, int n,
    FILE * restrict stream);

说明

fgets 函数最多读取比 n 指定的字符数少一个 从stream指向的流到s指向的数组。没有额外的 在换行符(保留)之后或文件结尾之后读取字符。一种 在读入数组的最后一个字符之后立即写入空字符。

退货

如果成功,fgets 函数返回 s。如果遇到文件结尾并且没有 字符已被读入数组,数组的内容保持不变,并且 返回空指针。如果在操作过程中发生读取错误,则数组内容为 不确定并返回一个空指针。

编辑:由于似乎问题在于对字符串是什么的误解,这是标准的相关摘录(强调我的):

7.1.1 术语定义

字符串是连续的字符序列以第一个 null 结尾并包括 字符。有时使用术语多字节字符串来强调特殊 对字符串中包含的多字节字符进行处理或避免混淆 用宽弦。指向字符串的指针是指向其初始(最低寻址)的指针 特点。 字符串的长度空字符和 字符串的值是包含的字符的值的顺序,按顺序排列。

【讨论】:

  • 你的意思是用 for 循环写入 7 个字符,然后读取结果字符串,也会是未定义的行为吗?我没有直接访问任何“非法”地址,这样做。我可以在buff[6] 中完美地写任何东西,然后阅读buff...或者这是标准禁止的吗? PS:你的证明是对的。为了证明什么,我需要知道这第 8 个'\0',专门针对字符串数组,是否属于标准的一部分!
  • 好吧,我做了一个困惑的评论。我想知道在分配(分配!)之后我可以立即看到的第 8 个'\0' 是因为符合标准而编写的,还是只是该系统上编译器的某些特殊性!如果总是有第 8 个 '\0',那么 fgets 编写的最后一个 '\0' 就变得不必要了
  • 不清楚\0 是标准的一部分是什么意思。该标准将以null结尾的字符序列定义为字符串,即最后一个字符为空字符(即数字代码为0的字符)的字符序列。如果你有一个字符数组,这个 可能 包含一个字符串。如果在其中某处有一个空字符,它会这样做。你不能考虑字符过去,它在概念上不存在
  • 更具体地说:如果您定义char ca[5],那么ca 包含一个字符串,如果它的任何元素是'\0'。更准确地说,字符串从ca[0] 开始,以包含'\0' 的(第一个)元素结束,它充当字符串终止符。此空字符必须在数组中。没有ca[6]。如果您编写这样的表达式,您将调用未定义的行为。您可能有一个内存位置,可能会被表达式ca[6] 访问,但这是非法的。 C 不检查带有 UB 的东西是否非法。 根据定义。在这种情况下发生的任何事情都是不可预测的
  • 你最后两个cmets回答了我的问题。我不知道 char 数组和字符串之间的这种区别。所以这个 ca[6] 设置为 0 只是为了方便编译器,这样可以“拯救”程序免于崩溃,无论是好是坏。谢谢!
【解决方案2】:

来自 C11 标准草案:

fgets 函数最多读取比 n 指定的字符数少一个 从stream指向的流到s指向的数组。无需额外 在换行符(保留)之后或文件结尾之后读取字符。 一个 在读入数组的最后一个字符之后立即写入空字符。

如果成功,fgets 函数返回 s。如果遇到文件结尾并且没有 字符已被读入数组,数组的内容保持不变,并且 返回空指针。如果在操作过程中发生读取错误,则数组内容不确定,返回空指针。

您描述的行为未定义。

【讨论】:

  • 这个我知道。我只是想知道我什至在我向字符串写入任何内容之前(即在分配时)看到的第 8 个'\0' 是否仅仅是偶然的,或者符合标准的另一段,例如,在一个之后的附加字节char[] 数组,在分配数组时将始终写为 '\0'...
  • 如果你幸运的话,数组会被填充,这样最后你可能会访问一两个字节——但你不应该依赖这个(未定义的行为)。
  • Lorenzo Donati 告诉我原因(字符数组和字符串之间的区别)。明白了,这只是编译器的运气,或者其他什么!
猜你喜欢
  • 1970-01-01
  • 2012-11-08
  • 2013-10-12
  • 2021-09-30
  • 1970-01-01
  • 2023-03-14
  • 2020-08-10
  • 1970-01-01
  • 2015-03-21
相关资源
最近更新 更多