【问题标题】:Different behaviour of Ctrl-D (Unix) and Ctrl-Z (Windows)Ctrl-D (Unix) 和 Ctrl-Z (Windows) 的不同行为
【发布时间】:2017-05-04 12:10:46
【问题描述】:

根据标题,我试图了解 Ctrl+D / Ctrl+Z 的确切行为在一个带有gets的while循环中(我需要使用它)。我正在测试的代码如下:

#include <stdio.h>
#include <stdlib.h>

int main()
{

    char str[80];

    while(printf("Insert string: ") && gets(str) != NULL) {

        puts(str);
    }

    return 0;
}

如果我的输入只是 Ctrl+D(或 Windows 上的 Ctrl+Z)@987654322 @ 返回 NULL,程序正确退出。不清楚的情况是当我插入类似house^D^D(Unix)或house^Z^Z\n(Windows)的东西时。

  1. 在第一种情况下,我的解释是 getchar(或 gets 函数中的类似内容)等待 read() 获取输入,第一个 Ctrl+D 刷新非空缓冲区(因此不是 EOF),然后触发第二次 read() 调用 EOF。
  2. 在第二种情况下,我注意到第一个 Ctrl+Z 被插入到缓冲区中,而后面的所有内容都被忽略了。因此,我的理解是插入house^Z 的第一个 read() 调用并丢弃返回 5(读取的字符数)的所有其他内容。 (我说 5 是因为否则我认为一个简单的 Ctrl+Z 应该返回 1 而不会触发 EOF)。然后程序等待来自用户的更多输入,因此第二次 read() 调用。

我想知道我对它的工作方式的正确和错误以及它的哪一部分只是依赖于实现(如果有的话)。


此外,我注意到在 Unix 和 Windows 中,即使在触发 EOF 之后,它似乎在以下 gets() 调用中重置为 false,我不明白为什么会发生这种情况以及代码的哪一行。

我非常感谢任何形式的帮助。


(2016 年 12 月 20 日)为了避免混淆,我大量编辑了我的问题

【问题讨论】:

  • 这个表达式:|| !feof(stdin)) 没有任何用处。主要是因为函数:feof() 检查代码是否试图读取 EOF,并且这个表达式只有在遇到 EOF 时才会检查,所以总是错误的。由于该功能不符合(通常)预期,强烈建议不要使用它。
  • 这个表达式:*curr++ 可以/将是一个问题。问题是因为C中运算符的优先级。建议:*curr = (char)c; curr++;
  • 我使用 !feof(stdin) 检查是否遇到了读取错误而不是 eof。我只是试图模拟 get(...) 函数,以便更好地隔离我怀疑的根源(即 getchar())
  • 如果出现 I/O 错误,函数:feof() 不会告诉您该错误。而是:#include &lt;errno.h&gt; 然后在调用 getchar() errno = 0; 之后紧接着调用 getchar() if( 0 != errno ) // handle error
  • 在大多数面向 Windows 的现代编译器上,_WIN32 宏是预定义的。您可以使用它在*curr++ = c; 行之前有条件地包含if (c != 0x1a)。这将避免将^Z 存储在输入缓冲区中并等待更多输入,类似于Unix 终端驱动程序不会将^D 传递给house^D 中的stdin。除此之外,您的功能看起来是正确的。

标签: c windows unix stdin


【解决方案1】:

CTRL-D 和 CTRL-Z“文件结束”指示符分别在 Unix 和 Windows 系统上起到类似的作用,但实现方式完全不同。

在 Unix 系统(包括 Linux 等 Unix 克隆)上,CTRL-D 虽然官方描述为文件结尾字符,但实际上是一个分隔符。它与用于分隔行的行尾字符(通常是回车符或 CTRL-M)的作用几乎相同。这两个字符都告诉操作系统输入行已完成并使其可用程序。唯一的区别是,对于行尾字符,在输入缓冲区的末尾插入换行符 (CTRL-J) 以标记行尾,而对于文件结尾字符,则不插入任何内容.

这意味着当您在 Unix 上输入 house^D^D 时,read 系统调用将首先返回一个长度为 5 的缓冲区,其中包含 5 个字符 house。当再次调用read 以获得更多输入时,它将返回一个长度为0 的缓冲区,其中没有字符。由于在普通文件上读取的零长度表示已到达文件末尾,gets 库函数也将其解释为文件末尾并停止读取输入。但是,由于它用 5 个字符填充了缓冲区,因此它不会返回 NULL 来指示它已到达文件末尾。而且由于它实际上并没有真正到达文件末尾,因为终端设备实际上不是文件,因此在此之后进一步调用gets 将进一步调用read,这将返回用户键入的任何后续字符。

在 Windows 上,CTRL-Z 的处理方式大不相同。最大的区别是它根本没有被操作系统特殊对待。当您在 Windows 上键入 house^Z^Z^M 时,只会对回车符进行特殊处理。就像在 Unix 上一样,回车使输入的行对程序可用,尽管在这种情况下,回车和换行被添加到缓冲区以标记行的结尾。所以结果是 ReadFile 函数返回一个 9 字节长的缓冲区,其中包含 9 个字符 house^Z^Z^M^J

实际上是程序本身,特别是 C 运行时库,对 CTRL-Z 进行了特殊处理。对于 Microsoft C 运行时库,当它在ReadFile 返回的缓冲区中看到 CTRL-Z 字符时,它会将其视为文件结束标记并忽略其后的所有内容。使用上一段中的示例,gets 最终会调用 ReadFile 以获得更多输入,因为从控制台(或其他设备)读取时不会记住它看到的 CTRL-Z 字符这一事实并且它没有还看到了行尾(被忽略了)。如果你再次按下回车,gets 将返回填充了 7 个字节 house^Z\0 的缓冲区(添加一个 0 字节表示字符串的结尾)。默认情况下,当从普通文件中读取时,它会做同样的事情,如果 CTRL-Z 字符出现在文件中,它和它之后的所有内容都会被忽略。这是为了向后兼容 CP/M,它只支持长度为 128 的倍数的文件,并使用 CTRL-Z 来标记文本文件真正应该结束的位置。

请注意,上面描述的 Unix 和 Windows 行为都只是用户输入的正常默认处理。 CTRL-D 的 Unix 处理仅在以规范模式从终端设备读取时发生,并且可以将“文件结尾”字符更改为其他字符。在 Windows 上,操作系统从不特别对待 CTRL-Z,但 C 运行时库是否这样做取决于正在读取的 FILE 流是文本模式还是二进制模式。这就是为什么在可移植程序中,在打开二进制文件(例如fopen("foo.gif", "rb"))时,您应该始终在模式字符串中包含字符b

【讨论】:

  • 首先我要感谢您对不同实现的深入分析。但是我注意到 CTRL-Z 在 Windows 上的处理方式略有不同,特别是如果我输入 house^Z^Z^Z\n,则只会忽略第二个和第三个“^Z”。
  • 这意味着puts(str) 在屏幕上打印字符串“house^Z”(其中^Z 打印为非ASCII 字符)。我的解释是,gets 内的getchar() 调用仅在ReadFile 中返回EOF:a) 没有要读取的字符(文件结尾)b) 遇到^Z 作为缓冲区中的第一个字符。如果^Z 在缓冲区的中间,它只会触发缓冲区的关闭,但getchar() 仍然返回“^Z”。 PS:我指的是gets 的实现,例如user3856986 上面的getS
  • 实际上忽略了第一个 ^Z 之后的所有内容,至少在 Microsoft C 运行时库实现中是这样。其他实现是可能的,比如发布的user3856986,虽然那个不符合要求,所以我不确定它是否真的用于任何真正的 C 运行时库,更不用说 Windows 了。如何准确处理 ^Z 将取决于实现以及可能的其他细节,例如流的缓冲模式。
  • @Elanigiro 我已更新答案以解决出现在 Windows 输出中的 CTRL-Z 字符。这实际上似乎是行为的变化,我认为旧版本的 Microsoft CRT 不会包含 CTRL-Z 字符。
猜你喜欢
  • 2013-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-21
  • 1970-01-01
  • 2020-02-20
  • 2015-02-11
相关资源
最近更新 更多