提问者:小点点

如何在C语言中读取/解析输入? 常见问题


当我试图读取/解析输入时,我的C程序出现了问题。

帮忙?

这是一个常见问题条目。

StackOverflow有许多与在C语言中读取输入有关的问题,答案通常集中在特定用户的特定问题上,而没有真正描绘整个画面。

这是一个试图全面地涵盖一些常见错误的尝试,因此这一组特定的问题可以简单地通过将它们标记为这一个问题的重复来回答:

  • 为什么最后一行打印两次?
  • 为什么我的扫描(“%d”,。。。)/扫描(“%c”,。。。)失败?
  • 为什么gets()崩溃?
  • 。。。

答案被标记为社区维基。 随时改进和(谨慎地)扩展。


共1个答案

匿名用户

  • 文本模式与二进制模式
  • 检查fopen()是否失败
  • 陷阱
    • 检查您调用的所有函数是否成功
    • EOF,或“为什么最后一行打印两次”
    • 永远不要使用gets()
    • 不要对stdin或任何其他打开读取的流使用fflush(),永远不要
    • 不要对可能格式错误的输入使用*scanf()
    • 当*scanf()未按预期工作时
    • 通过fgets()读取一行输入的(部分)
    • 分析内存中的行

    “二进制模式”流的读入完全与它被写入时一样。 但是,可能(也可能没有)在流的末尾追加了实现定义的null字符数('\0')。

    “文本模式”流可以进行多个转换,包括(但不限于):

    • 删除行尾前的空格;
    • 在输出时将换行('\n')更改为其他内容(例如,Windows上的“\r\n”),并在输入时返回到'\n'
    • 添加,更改或删除既不是打印字符(IsPrint(c)为true),也不是水平制表符或新行的字符。

    应该很明显,文本和二进制模式是不混合的。 以文本模式打开文本文件,以二进制模式打开二进制文件。

    打开文件的尝试可能由于各种原因而失败--最常见的原因是缺少权限或未找到文件。 在这种情况下,fopen()将返回一个null指针。 在尝试读取或写入文件之前,始终检查fopen是否返回null指针。

    fopen失败时,它通常会设置全局errno变量来指示失败的原因。 (这在技术上不是C语言的要求,但POSIX和Windows都保证做到这一点。) errno是可以与errnoH中的常量进行比较的代码号,但是在简单的程序中,通常您所需要做的就是使用perror()strerror()将其转换为错误消息并打印出来。 错误消息还应包括您传递给fopen的文件名; 如果您不这样做,当问题是文件名不是您认为的那样时,您将非常困惑。

    #include <stdio.h>
    #include <string.h>
    #include <errno.h>
    
    int main(int argc, char **argv)
    {
        if (argc < 2) {
            fprintf(stderr, "usage: %s file\n", argv[0]);
            return 1;
        }
    
        FILE *fp = fopen(argv[1], "rb");
        if (!fp) {
            // alternatively, just `perror(argv[1])`
            fprintf(stderr, "cannot open %s: %s\n", argv[1], strerror(errno));
            return 1;
        }
    
        // read from fp here
    
        fclose(fp);
        return 0;
    }
    

    检查您调用的任何函数是否成功

    这应该是显而易见的。 但是请检查您调用的任何函数的文档,以获取它们的返回值和错误处理,并检查这些条件。

    这些错误是很容易发生的,当你发现的条件很早,但导致许多挠头,如果你没有。

    EOF,或“为什么最后一行打印两次”

    如果达到EOF,函数feof()返回true。 对“达到”EOF实际含义的误解使得许多初学者写了这样的东西:

    // BROKEN CODE
    while (!feof(fp)) {
        fgets(buffer, BUFFER_SIZE, fp);
        printf("%s", buffer);
    }
    

    这使得输入的最后一行被打印两次,因为当最后一行被读取时(直到最后一个换行,即输入流中的最后一个字符),EOF没有被设置。

    EOF仅在您尝试读取最后一个字符时设置!

    因此上面的代码再次循环,fgets()无法读取另一行,设置EOF并保留buffer的内容,然后再次打印。

    而是检查fgets是否直接失败:

    // GOOD CODE
    while (fgets(buffer, BUFFER_SIZE, fp)) {
        printf("%s", buffer);
    }
    

    永远不要使用gets()

    没有办法安全地使用这个功能。 正因为如此,随着C11的出现,它已经从语言中被移除。

    永远不要在stdin或任何其他打开读取的流上使用fflush()

    许多人期望fflush(stdin)丢弃尚未读取的用户输入。 它不会那样做。 在普通的ISO C中,对输入流调用fflush()具有未定义的行为。 它在POSIX和MSVC中确实有定义良好的行为,但这两种行为都不会使它丢弃尚未读取的用户输入。

    通常,清除挂起输入的正确方法是读取并丢弃新行以内(含新行)的字符,但不能超过:

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

    对于可能格式错误的输入,不要使用*scanf()

    许多教程教您使用*scanf()读取任何类型的输入,因为它是如此的通用。

    但是*scanf()的目的实际上是读取可以依赖于预定义格式的大容量数据。 (例如由另一个程序编写。)

    即便如此,*scanf()也可以跳过不可观察的:

    • 使用在某种程度上可能受用户影响的格式字符串是一个巨大的安全漏洞。
    • 如果输入与预期格式不匹配,*scanf()将立即停止解析,留下任何未初始化的剩余参数。
    • 它会告诉您它成功完成了多少次赋值--这就是为什么您应该检查它的返回代码(见上文)--但不会告诉您它停止解析输入的确切位置,这使得很难进行优雅的错误恢复。
    • 它跳过输入中的任何前导空格,除非它没有([CN转换)。(请参阅下一段。)
    • 在某些特殊情况下,它有一些特殊的行为。

    当*scanf()不按预期工作时

    *scanf()的一个常见问题是当输入流中存在用户没有说明的未读空格(''\n',。。。)时。

    读取数字(“%d”等)或字符串(“%s”)时,会在任何空格处停止。 虽然大多数*scanf()转换说明符会跳过输入中的前导空格,但[CN不会跳过。因此换行符仍然是第一个挂起的输入字符,这使得%C%[无法匹配。

    您可以跳过输入中的换行,方法是显式读取它,例如通过fgetc(),或者在*scanf()格式字符串中添加一个空白。 (格式字符串中的单个空格匹配输入中的任意数量的空格。)

    我们只是建议不要使用*scanf(),除非您确实知道自己在做什么。 那么,用什么作为替代品呢?

    不是像*scanf()尝试的那样一次性读取和解析输入,而是将这些步骤分开。

    通过fgets()读取一行输入的(部分)

    fgets()有一个参数,用于限制其输入至多为那么多字节,以避免缓冲区溢出。 如果输入行完全适合您的缓冲区,则缓冲区中的最后一个字符将是换行('\n')。 如果它不完全适合,那么您看到的是一个部分读取的行。

    解析内存中的行

    对于内存解析特别有用的是strtol()和strtod()函数族,它们提供与*scanf()转换说明符diuoxaefg类似的功能。

    但是它们也会准确地告诉您它们停止解析的位置,并且有意义地处理对目标类型来说太大的数字。

    除此之外,C还提供了广泛的字符串处理功能。 由于您在内存中有输入,并且始终确切地知道您已经解析了它多远,因此您可以多次返回,试图理解输入的意义。

    如果其他所有操作都失败,则可以使用整行代码来为用户打印有用的错误消息。

    确保您显式关闭任何您已经(成功)打开的流。 这将刷新所有尚未写入的缓冲区,并避免资源泄漏。

    fclose(fp);
    

相关问题