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

时间:2016-02-03 13:32:51

标签: c stdio

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

帮助?

这是一个FAQ条目。

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

这是一次全面覆盖一些常见错误的尝试,所以只需将这些错误标记为重复,就可以回答这一特定问题:

  • 为什么最后一行打印两次?
  • 为什么我的scanf("%d", ...) / scanf("%c", ...)会失败?
  • 为什么gets()会崩溃?
  • ...

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

1 个答案:

答案 0 :(得分:27)

初学者的C输入入门

  • 文本模式与二进制模式
  • 检查fopen()是否失败
  • 陷阱
    • 检查您要求成功的所有功能
    • EOF,或"为什么最后一行打印两次"
    • 永远不要使用gets()
    • 不要在stdin上使用fflush()或任何其他可供阅读的流,
    • 请勿将*scanf()用于可能格式错误的输入
    • *scanf()未按预期运作时
  • 读取,然后解析
    • 通过fgets()
    • 阅读(部分)输入
    • 解析内存中的行
  • 清理

文本模式与二进制模式

A"二进制模式"流的读取方式与写入的完全相同。但是,可能(或可能不)是在流末尾附加的实现定义数量的空字符(' \0')。

A"文字模式"流可以进行多种转换,包括(但不限于):

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

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

检查fopen()是否失败

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

fopen失败时,通常会设置全局errno变量,以指示 失败的原因。 (这在技术上不是C语言的要求,但POSIX和Windows都保证这样做。)errno是一个代码号,可以与errno.h中的常量进行比较,但在简单的程序中,通常您需要做的就是将其变为错误消息并使用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,或&#34;为什么最后一行打印两次&#34;

如果已达到EOF,则函数feof()将返回true。误解了什么&#34;达到&#34; EOF实际上意味着让许多初学者写下这样的东西:

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

这使得输入的最后一行打印两次,因为当读取最后一行时(直到最后一行,输入流中的最后一个字符), EOF是<强>不设置。

当您尝试阅读过去最后一个字符时,EOF才会被设置!

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

相反,请检查fgets是否直接失败:

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

永远不要使用gets()

There is no way to use this function safely.因此,随着C11的出现,语言已被删除

请勿在{{1​​}}上使用fflush()或任何其他可供阅读的信息流

许多人希望stdin放弃尚未阅读的用户输入。 它没有那样做。在普通的ISO C中,在输入流上调用fflush()undefined behaviour。它确实在POSIX和MSVC中具有明确定义的行为,但这些行为都不会丢弃尚未读取的用户输入。

通常,读取清除待处理输入的正确方法并丢弃字符,包括换行符,但不得超出:

fflush(stdin)

请勿将*scanf()用于可能格式错误的输入

许多教程教您使用*scanf()来阅读任何类型的输入,因为它非常通用。

*scanf()的目的实际上是读取大量数据,这些数据在处于预定义格式时可能会有些依赖。 (比如被另一个程序写的。)

即便如此,*scanf()也可以绊倒那些不注意的人:

  • 使用以某种方式可能会受到用户影响的格式字符串是一个巨大的安全漏洞。
  • 如果输入与预期的格式不匹配,*scanf()会立即停止解析,而剩余的参数将保持未初始化状态。
  • 它会告诉你它成功完成了多少作业 - 这就是为什么你应该检查它的返回代码(见上文) - 但不是它的确切位置停止解析输入,使得优雅的错误恢复变得困难。
  • 它会跳过输入中的所有前导空格,除非它没有(int c; do c = getchar(); while (c != EOF && c != '\n'); [c次转换)。 (见下一段。)
  • 在一些极端情况下,它有一些奇特的行为。

*scanf()无效时

*scanf()的一个常见问题是当输入流中有未读空格(n' ',...)时,用户没有考虑到这一点。

读取数字('\n'等)或字符串("%d")会在任何空格处停止。虽然大多数"%s"转换说明符在输入中跳过前导空格,但*scanf()[c却没有。因此,换行符仍然是第一个待处理的输入字符,导致n%c无法匹配。

您可以通过明确阅读它来跳过输入中的换行符,例如通过fgetc(),或通过在*scanf()格式字符串中添加空格。 (格式字符串中的单个空格与输入中的任何空格数相匹配。)

读取,然后解析

我们只是建议不要使用*scanf(),除非你真的,积极地知道你在做什么。那么,作为替代品使用什么?

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

通过fgets()

读取一部分输入(部分)

fgets()有一个参数,用于将其输入限制为最多多个字节,从而避免缓冲区溢出。如果输入行确实完全适合您的缓冲区,缓冲区中的最后一个字符将是换行符(%[)。如果它并不完全合适,那么你正在看一条部分读取的行。

解析内存中的行

特别适用于内存中解析的是strtol()strtod()函数系列,它们提供与*scanf()转换说明符'\n'd类似的功能,iuoxaef

但是他们也告诉你完全他们停止解析的地方,并且对目标类型的数字进行了有意义的处理。

除此之外,C提供wide range of string processing functions。由于你已经在内存中输入,并且总是知道你已经解析了多远,你可以回过头来尝试理解输入。

如果所有其他方法都失败了,您可以使用整行来为用户打印有用的错误消息。

清理

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

g