使用EOF(无需回车)结束循环

4

我目前正在尝试使用以下代码结束while循环:

#include <stdio.h>
int main() 
{
    while(getchar() != EOF)
    {
        if( getchar() == EOF )
            break;
    }
    return 0;

当我在Ubuntu上按下CTRL+D时,它会立即结束循环。但在Windows上,我必须按CTRL+Z然后按ENTER来关闭循环。我能否在Windows上去掉ENTER


你是不是在按下回车键后立即按下CTRL+Z了? - Ctx
是的,我在开始新行后直接完成了它。 - Michael Hübler
1
你的循环调用了1次过多的getchar() - 你只需要一次,并将其返回的值赋给一个变量,以便在循环内使用。 - Chris Turner
2个回答

7

getchar的行为

对于Linux系统,EOF字符可以通过ctrl+d来输入,而在Windows系统中,当你通过ctrl+z改变CRT库的内部状态后,通过按下enter键来写入该字符(这种行为是为了与非常老的系统保持兼容性)。如果我没记错的话,它被称为软文件结尾。我认为你无法绕过它,因为当你按下enter键时,EOF字符实际上已被你的getchar函数消耗掉,而不是在你按下ctrl+z时。

正如这里所述:

在Microsoft的DOS和Windows中(以及在CP/M和许多DEC操作系统中),从终端读取永远不会产生EOF。相反,程序会识别源为终端(或其他“字符设备”),并将给定的保留字符或序列解释为文件结束指示符;最常见的是ASCII控制字符-Z,代码26。一些MS-DOS程序,包括微软MS-DOS shell(COMMAND.COM)的部分和操作系统实用程序(如EDLIN),将文本文件中的Control-Z视为标记有意义数据的结尾,并/或在写入文本文件时在末尾附加Control-Z。这是出于两个原因:

  • 与CP/M向后兼容。CP/M文件系统仅以128字节“记录”的倍数记录文件长度,因此按照惯例,如果数据在记录的中间结束,则使用Control-Z字符标记有意义数据的结尾。MS-DOS文件系统始终记录文件的确切字节长度,因此在MS-DOS上从未必要。

  • 它允许程序使用相同的代码从终端和文本文件读取输入。

其他信息也可以在这里找到:

一些现代文本文件格式(例如CSV-1203)仍建议在文件末尾附加一个尾随EOF字符。然而,在MS-DOS或Microsoft Windows中,键入Control+Z不会将EOF字符嵌入文件中,系统的API也不使用该字符来表示实际的文件结尾。

一些编程语言(例如Visual Basic)在使用内置的文本文件读取基元(INPUT、LINE INPUT等)时,不会读取“软”EOF之后的内容,必须采用其他方法,例如以二进制模式打开文件或使用文件系统对象来超越它。

字符26被用于标记“文件结尾”,即使ASCII将其称为替代符号,并且还有其他字符可以用于此目的。

如果您将您的代码修改为以下形式:
#include <stdio.h>

int main() {
  while(1) {
    char c = getchar();
    printf("%d\n", c); 
    if (c == EOF)      // tried with also -1 and 26
      break;
  }
  return 0;
}

如果你在Windows上测试它,你会发现只有在按下enter键之前,控制台才不会显示EOF (-1)。在此之前,终端仿真器会打印一个^Z(我猜测)。从我的测试来看,如果:

  • 使用Microsoft编译器编译
  • 使用GCC编译
  • 在CMD窗口中运行已编译的代码
  • 在Windows中运行bash仿真器中的已编译代码

使用Windows控制台API进行更新

根据@eryksun的建议,我成功地编写了一段(为它可以做的事情而言过于复杂)的Windows代码,改变了conhost的行为,以实际获得“按ctrl+d退出”的功能。它并不能处理所有情况,它只是一个例子在我看来,这是要尽可能避免的事情,因为它的可移植性小于0。此外,为了正确处理其他输入情况,需要编写更多的代码,因为这个东西将stdin从控制台分离出来,你必须自己处理。

该方法的工作原理如下:

  • 获取标准输入的当前处理程序
  • 创建一个输入记录数组,该结构包含有关conhost窗口中发生的事件的信息(键盘、鼠标、调整大小等)
  • 读取窗口中发生的事件(它可以处理事件数量)
  • 遍历事件向量以处理键盘事件并截获所需的EOF(从我测试的结果来看,它是4),或打印任何其他ascii字符。

这是代码:

#include <windows.h>
#include <stdio.h>

#define Kev input_buffer[i].Event.KeyEvent // a shortcut

int main(void) {
  HANDLE h_std_in;                // Handler for the stdin
  DWORD read_count,               // number of events intercepted by ReadConsoleInput
        i;                        // iterator
  INPUT_RECORD input_buffer[128]; // Vector of events

  h_std_in = GetStdHandle( // Get the stdin handler
    STD_INPUT_HANDLE       // enumerator for stdin. Others exist for stdout and stderr
  ); 

  while(1) {
    ReadConsoleInput( // Read the input from the handler
      h_std_in,       // our handler 
      input_buffer,   // the vector in which events will be saved
      128,            // the dimension of the vector
      &read_count);   // the number of events captured and saved (always < 128 in this case)

    for (i = 0; i < read_count; i++) {    // and here we iterate from 0 to read_count
      switch(input_buffer[i].EventType) { // let's check the type of event 
        case KEY_EVENT:                   // to intercept the keyboard ones
          if (Kev.bKeyDown) {             // and refine only on key pressed (avoid a second event for key released)
            // Intercepts CTRL + D
            if (Kev.uChar.AsciiChar != 4)
              printf("%c", Kev.uChar.AsciiChar);
            else
              return 0;
          }
          break;
        default:
          break;
      }
    }
  }

  return 0;
}

在其核心,控制台没有预定的控制字符来指示不按回车键即可返回正常。如果一行以Ctrl+Z("\x1a")开头,则“ReadFile”的实现将返回0个字符读取,并且该行为在C运行时中也很常见。控制台本身(conhost.exe)没有这样的行为,如果您调用ReadConsoleW(用于Unicode支持),则必须手动实现此操作。话虽如此,ReadConsoleW具有更好的行为,正是OP想要的--它的pInputControl参数可以实现类似Unix的Ctrl+D。 - Eryk Sun
请注意,这里没有CMD窗口。CMD是一个标准的I/O应用程序,可以像其他控制台应用程序一样使用控制台(conhost.exe实例)。它具有控制台输入和屏幕缓冲区的标准句柄,并且可以访问与任何其他Windows应用程序相同的控制台API函数(例如GetConsoleModeSetConsoleTitle等)。 - Eryk Sun
谢谢,我已经根据您的建议更新了答案,并且出现了“EOF退出”...这太糟糕了(主要是因为我以前从未关心过使用winapi,并且显然不擅长),但我不想花太多时间在这上面。此外,我理解您所说的“没有cmd窗口”。对我来说,C:\ Windows \ system32 \ cmd.exe就是命令窗口。使用conhost和Windows API的事实并不改变它在普通Windows安装中默认为CMD的事实。 - Matteo Ragni
cmd.exe从来不是Windows的控制台,也从未在任何版本中是。cmd.exe与powershell.exe或python.exe一样都不是控制台。CMD不会创建窗口,甚至不会加载user32.dll。它是一个简单的控制台客户端,使用其StandardInputStandardOutputStandardError句柄进行控制台输入和屏幕缓冲区,或重定向到管道、文件、NUL等。作为控制台应用程序,它还具有连接到其附加控制台的连接句柄,这是PEB ProcessParamaters中的ConsoleHandle。许多控制台函数隐式地使用此句柄。 - Eryk Sun
我建议使用带有 pInputControl 参数的 ReadConsoleW,而不是低级别的 ReadConsoleInput 函数。具体来说,我建议使用 dwCtrlWakeupMask 位掩码,例如 inputCtrl.dwCtrlWakeupMask = 1 << ('D' - '@') | 1 << ('Z' - '@')。它会在字符串中保留控制字符(例如 L"\x04"L"\x1A"),因此需要您的代码搜索并执行所需操作(例如将其替换为 NUL)。 - Eryk Sun

1
    while(getchar() != EOF)
    {
        if( getchar() == EOF )
            break;
    }
    return 0;

这里是不一致的。
如果 `getchar() != EOF`,它将进入循环,否则(如果 `getchar() == EOF`)它将不会进入循环。因此,在循环内部没有理由检查 `getchar() == EOF`。
另一方面,您调用了 `getchar()` 两次,您需要等待输入两个字符而不是一个。
你试图做什么?

这么多年后才看到这个,我非常确定应该只有一个 getchar()。 - Michael Hübler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接