【问题标题】:How to read / parse input in C? The FAQ如何在 C 中读取/解析输入?常见问题解答
【发布时间】:2016-05-12 17:41:00
【问题描述】:

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

帮助?


这是一个常见问题条目。

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

这是试图全面涵盖一些常见错误,因此只需将这一系列问题标记为与此问题重复即可:

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

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

【问题讨论】:

  • 对我来说就像一个教程。好主意,但 imo 不支持。
  • @Olaf:我生来就给出了基本相同的答案,只是略有不同,多年来很多次,因为真的没有一个好的“涵盖所有内容”用于重复关闭的答案。 This one,以及 chux 对此的反应,终于让我写了一篇,以 c++-faq 条目的风格。
  • 抱歉,虽然我承认你的努力并完全理解你厌倦了每天得到同样的 sh**,但这个主题太宽泛了。例如在哪里您是否设置了使用正确的词法分析器/解析器方法的阈值?或单个字符读取与strtoXXX 等。正如您所写:每个问题都有细微的变化。对于重复的问题,已经建立了一个程序。大多数答案都更紧凑。请注意,提问者遇到的一个主要问题是抽象化并根据他们的问题调整一般概念。像这样的通用教程(不是常见问题解答)不会改变这一点。
  • 如果这是尝试创建一个“规范副本”,我们可以使用它来关闭有关 scanf 和 stdin 中剩余换行符的新手常见问题解答,那么我完全支持它。
  • 我肯定会说这种“问题”在这里太宽泛了......

标签: c stdio


【解决方案1】:

初学者的 C 输入入门入门

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

文本模式与二进制模式

“二进制模式”流的读取与写入时完全相同。但是,流的末尾可能会(也可能不会)附加实现定义的空字符数('\0')。

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

  • 删除行尾前的空格;
  • 在输出时将换行符 ('\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], "r");
    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()

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

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

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

通常,清除待处理输入的正确方法是读取并丢弃直到换行符(包括换行符)的字符,但不能超过:

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

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

许多教程教你使用*scanf() 来读取任何类型的输入,因为它用途广泛。

*scanf() 的目的实际上是读取在某种程度上依赖预定义格式的批量数据。 (比如被另一个程序写的。)

即使这样*scanf() 也会绊倒不注意的人:

  • 使用在某种程度上会受到用户影响的格式字符串是一个巨大的安全漏洞。
  • 如果输入与预期格式不匹配,*scanf() 会立即停止解析,而剩余的所有参数都未初始化。
  • 它会告诉你它已经成功完成了多少任务——这就是为什么你应该检查它的返回码(见上文)——但不是它的确切位置停止解析输入,使得优雅的错误恢复变得困难。
  • 它会跳过输入中的任何前导空格,除非它不跳过([cn 转换)。 (见下一段。)
  • 在某些极端情况下,它的行为有些特殊。

*scanf() 没有按预期工作时

*scanf() 的一个常见问题是输入流中存在用户未考虑的未读空格(' ''\n'、...)。

读取数字("%d" 等)或字符串("%s")会在任何空白处停止。虽然大多数 *scanf() 转换说明符 skip 在输入中前导空格,但 [cn 不会。所以换行符仍然是第一个待处理的输入字符,导致 %c%[ 无法匹配。

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

读取,然后解析

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

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

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

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

解析内存中的行

对于内存解析特别有用的是 strtol()strtod() 函数系列,它们提供与 *scanf() 转换说明符 diuo、@ 类似的功能987654398@、aefg

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

除此之外,C 还提供wide range of string processing functions。由于您将输入保存在内存中,并且始终知道您已经解析了多远,因此您可以多次返回以尝试理解输入。

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

清理

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

fclose(fp);

【讨论】:

  • 以文本模式而不是二进制模式打开文件的示例,对于初学者指南来说,将更适用。 fopen(argv[1], "rb"); --> fopen(argv[1], "r");
  • 这将是一个很好的答案,但是您没有提到任何与 fgetcfgets 相关的陷阱,因此您的“GOOD CODE”被严重破坏.
猜你喜欢
  • 2017-04-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-01-12
  • 1970-01-01
  • 2011-02-26
  • 2011-12-27
相关资源
最近更新 更多