【问题标题】:puts(), gets(), getchar(), putchar() function simultaneously use in the programputs()、gets()、getchar()、putchar()函数在程序中同时使用
【发布时间】:2021-05-25 09:52:11
【问题描述】:

我对在代码中同时使用puts()gets()putchar()getchar() 感到困惑。

当我运行以下代码时,它正在执行所有步骤: 获取输入,打印输出,再次获取输入,打印输出。

#include <stdio.h>

int main() {
    char ch[34];
    gets(ch);
    puts(ch);

    char g;
    g = getchar();
    putchar(g);
}

输出:

Priyanka
Priyanka
J
J

但是,当我使用此代码时: 它只做两个步骤: 接受输入,打印输入,然后是一行空间。我不明白为什么它会这样。

代码:

#include <stdio.h>

int main() {
    char g;
    g = getchar();
    putchar(g);   
 
    char ch[34];
    gets(ch);
    puts(ch);
    getch();
}

输出:

P
P

【问题讨论】:

  • gets() 在 C 和 C++ 标准中均已弃用。用起来也很危险。
  • 请参阅 Why gets() is too dangerous to be used,详细讨论为什么永远不应该使用 gets() 以及其他替代方案。
  • 请注意,上一个示例中使用的getch() 不是标准C 函数。
  • 第二个例子的操作顺序是:你输入“P”然后回车(终端驱动显示的字母和换行符); getchar() 返回字母“P”; putchar() 输出“P”,但尚未出现; gets() 读取换行符并返回一个空字符串; puts() 输出空字符串和换行符,也强制显示“P”。如果您输入“Priyanka”而不是“P”,您会得到类似的结果,“Priyanka”出现两次。如果您在putchar(g); 之后使用putchar('X') 并输入“Priyanka”,您会看到“PXriyanka”。
  • 请注意,生产代码需要检查输入操作是否成功,您还需要使用int g 而不是char g,因为getchar() 返回一个int,而不是一个char(在某些情况下差异确实很重要)。

标签: c getchar gets puts putchar


【解决方案1】:

代码中存在一些问题,输入机制比你想象的要复杂:

  • 您不应使用gets() 读取输入:此函数不能安全使用,因为它不接收有关目标数组大小的信息,因此任何足够长的输入行都会导致缓冲区溢出。它已从 C 标准中删除。您应该改用 fgets() 并处理缓冲区末尾的换行符。
  • g 应该有int 类型以容纳getc() 返回的所有值,即unsigned char 类型的所有值(在大多数当前系统中0255)和特殊的负值EOF (通常为 -1)。

这是修改后的版本:

#include <stdio.h>

int main() {
    char ch[34];
    if (fgets(ch, sizeof ch, stdin))
        fputs(ch, stdout);

    int g = getchar();
    if (g != EOF)
        putchar(g);
    return 0;
}

输出:

Priyanka
Priyanka
J
J

关于控制台响应程序输入请求的行为,它是由实现定义的,但通常涉及两层缓冲:

  • FILE 流包实现了一种缓冲方案,其中数据以块的形式从系统读取或写入系统。可以使用setvbuf() 控制此缓冲。有 3 种设置可用:无缓冲(stderr 的默认设置)、行缓冲(通常是stdinstdout 连接到字符设备时的默认设置)和完全缓冲的可自定义块大小(常见大小)是 512 和 4096)。
  • 当您调用getchar() 或更一般的getc(stream) 时,如果流的缓冲区中有可用的字节,则返回该字节并增加流位置,否则向系统发出请求以填充缓冲区。
  • 如果流附加到文件,填充缓冲区会执行read 系统调用或等效操作,除非在文件末尾或读取错误,否则会成功。
  • 如果流连接到字符设备,例如终端或图形显示上的终端窗口等虚拟 tty,则涉及另一层缓冲,设备驱动程序从输入设备读取输入并处理一些键以特殊方式,例如 Backspace 擦除前一个字符,光标移动键在输入行内移动,Ctrl-D (unix) 或 Ctrl-Z (windows) 表示文件结束。这层缓冲可以通过tcsetattr()系统调用或其他系统特定的API来控制。文本编辑器等交互式应用程序通常会禁用此功能并直接从输入设备检索原始输入。
  • 用户键入的键由终端处理以形成输入行,当用户键入 Enter 时发送回 C 流 API(翻译为系统特定的行尾序列),流函数执行另一组转换(即:在旧系统上将CR/LF 转换为'\n')并且字节行存储在流缓冲区中。当getc() 终于有机会返回第一个可用字节时,用户已经键入并输入了整行,并在流或设备缓冲区中挂起。

在这两个程序中,getchar() 不会返回从stdin 读取的下一个字节,直到从终端读取整行并存储在流缓冲区中。在第一个程序中,该行的其余部分在程序退出时被忽略,但在第二个程序中,该行的其余部分可供后续gets() 读取。如果您键入 JEnter,则读取的行是 J\ngetchar() 返回 'J',留下换行符 [在输入流中结束,然后是 @ 987654351@ 将读取换行符并返回一个空行。

【讨论】:

    【解决方案2】:

    在语句 putchar()gets() 我不建议使用的语句之间,丢弃输入流直到EOF 或直到出现换行符才能解决问题:

    .
    .
    int c;
    while ((c = getchar()) != EOF && c != '\n')
      ;
    .
    .
    

    我建议使用fgets(3),这样使用起来更安全,例如:

    char str[1024];
    if (fgets(str, sizeof str, stdin) == NULL) {
      // Some problem, handle the error...
    }
    
    // or, Input is okay...
    

    【讨论】:

      【解决方案3】:

      嗯,你这里有问题。您在第二个示例代码中使用了一个不属于 stdio 包的函数。

      你调用getch() 这不是一个标准输出函数。它是 ncurses 库的一部分,如果您没有在编译时指定您将使用它,那么您将无法获得可执行程序。所以这让我觉得你没有说实话。

      只需获取程序的函数getch() 即可获得完整行

      Priyanka
      

      输出,程序终止。我猜你使用getch() 来停止输出,直到你按下一个字符。但是由于curses库要求你在调用任何其他curses库函数之前调用initscr(),所以它没有正确初始化,因此你得到的输出可能是错误的。

      我不会重复其他人已经告诉你的关于gets() 的使用,它仍然在标准库中,并且知道你在做什么,你仍然可以在适当控制的环境中使用它。尽管如此,其他人给你的建议在这里并不适用,因为你没有溢出你使用的短缓冲区(只有 34 个字符,太短,太容易挂起你的程序或崩溃它)

      来自 stdio 的函数使用缓冲区,而 unix tty 驱动程序也在这里干扰。在您按下&lt;ENTER&gt; 键之前,您的终端不会使您输入到程序中的任何字符可用,然后程序会将所有这些字符读入缓冲区。它们从缓冲区中消耗,直到它为空,所以无论你一个一个地读取它们(使用fgetch(),还是一次全部读取(使用fgets() ---我将使用它,更多安全,功能,从现在开始)只要按下&lt;ENTER&gt; 键,一切都会发生。

      fgetch() 只占用一个字符,因此如果有多个可用字符,则只从缓冲区中取出一个字符,其余的等待轮到它们。但是fgets() 读取所有(并填充缓冲区)直到读取\n(这就是gets() 如此危险的原因,因为它不知道缓冲区的大小/它没有参数指示缓冲区的大小,因为fgets() 有/并且无法控制读取在溢出之前停止)

      因此,在您的情况下,当您按下一系列字符,然后按回车键时,第一个样本读取完整的字符串,然后第二个 getchar() 获取第二行的第一行(但您需要输入两个在那一点上完整的行)第二个示例在您调用getchar()时读取第一个字符,当您调用gets()时读取该行的其余部分。

      要一次读取一个字符,而无需等待输入一整行,终端驱动程序必须被编程为以原始模式读取字符。 unix 使用 Cookied 模式(默认)来读取完整的行,这允许您编辑、擦除行上的字符,并且只有在您准备好并按下&lt;ENTER&gt; 键时才输入它。

      如果您有兴趣从终端逐个读取字符,请阅读手册页termios(4),其中解释了tty 设备的接口和iocontrols。 curses 库进行必要的内务处理以将终端置于原始模式,以允许像 vi(1) 这样的程序逐个字符读取输入字符,但是您不需要直接使用 stdio,因为它的缓冲系统会吃掉您尝试的字符带着诅咒吃饭。

      【讨论】: