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

How to read / parse input in C? The FAQ

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

帮忙?


这是一个常见问题条目。

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

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

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

初学者的 C 输入入门

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

文本模式与二进制模式

“二进制模式”流被完全按照写入的方式读入。但是,可能(也可能不会)在流的末尾附加实现定义的空字符数 ('[=16=]')。

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

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

显然文本和二进制模式不能混用。以文本方式打开文本文件,以二进制方式打开二进制文件。

检查 fopen() 是否失败

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

fopen失败时,通常设置全局errno variable to indicate why it failed. (This is technically not a requirement of the C language, but both POSIX and Windows guarantee to do it.) errno is a code number which can be compared against constants in errno.h, but in simple programs, usually all you need to do is turn it into an error message and print that, using perror() or 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;
}

陷阱

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

这应该是显而易见的。但是 do 检查您调用的任何函数的文档以了解其 return 值和错误处理,并 check 了解这些条件。

当您及早发现情况时,这些错误很容易出现,但如果您不这样做,就会导致很多麻烦。

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

函数feof() returns true 如果已经到达EOF。对“达到”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() 具有 未定义的行为。它在 POSIX 和 MSVC 中确实有明确定义的行为,但它们都不会丢弃尚未读取的用户输入。

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

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

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

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

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

即使这样 *scanf() 也会绊倒粗心的人:

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

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

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

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

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

读取,然后解析

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

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

通过fgets()

读取(部分)一行输入

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

解析内存中的行

对内存解析特别有用的是 strtol()strtod() 函数系列,它们提供与*scanf() 转换说明符 diuoxaefg.

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

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

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

清理

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

fclose(fp);