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

Dev*_*lar 21 c stdio

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

救命?


这是一个FAQ条目.

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

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

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

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

Dev*_*lar 29

初学者的C输入入门

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

文本模式与二进制模式

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

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

  • 在线端之前移除空格;
  • 将newlines('\n')更改为输出"\r\n"上的其他内容(例如在Windows上)并返回到'\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;
}
Run Code Online (Sandbox Code Playgroud)

陷阱

检查您呼叫成功的任何功能

这应该是显而易见的.但是,检查您为其返回值和错误处理调用的任何函数的文档,并检查这些条件.

这些错误很容易在你早期发现这种情况时容易出现,但如果不这样做会导致很多人头疼.

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

如果已达到EOF,则函数feof()返回true.对"达到"EOF实际意味着什么的误解让许多初学者写下这样的东西:

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

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

EOF只有当你试图读取被设定过去的最后一个字符!

所以上面的代码再次循环,fgets()无法读取另一行,设置EOF buffer保留未触摸的内容,然后再次打印.

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

// GOOD CODE
while (fgets(buffer, BUFFER_SIZE, fp)) {
    printf("%s", buffer);
}
Run Code Online (Sandbox Code Playgroud)

永远不要使用gets()

无法安全使用此功能.因此,随着C11的出现,它已从语言中删除.

不要使用fflush()stdin或任何其他流进行读取操作,永远

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

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

int c;
do c = getchar(); while (c != EOF && c != '\n');
Run Code Online (Sandbox Code Playgroud)

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

许多教程教你使用*scanf()来读取任何类型的输入,因为它是如此多才多艺.

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

即使这样,*scanf()也可以使非观察者绊倒:

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

*scanf()无法按预期工作时

一个常见的问题)*scanf函数(当有未读空格(' ','\n'输入流,用户不占,...).

读取数字("%d"等)或字符串("%s")会在任何空格处停止.虽然大部分的*scanf()转换说明跳过输入前导空格,[,cn没有.因此,换行符仍然是第一个待处理的输入字符,无论是匹配%c还是%[不匹配.

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

阅读,然后解析

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

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

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

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

解析内存中的行

用于在存储器内的解析特别有用的是与strtol()关于strtod()函数族,其提供类似的功能的*scanf()的转换说明d,i,u,o,x,a,e,f,和g.

但是他们也告诉你他们停止解析的确切位置,并对目标类型的数字进行有意义的处理.

除此之外,C还提供各种字符串处理功能.由于你已经在内存中输入了输入,并且总是知道你已经解析了多远,你可以回过头来尝试理解输入.

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

清理

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

fclose(fp);
Run Code Online (Sandbox Code Playgroud)

  • 这将是一个很好的答案,但您没有提到任何与“fgetc”或“fgets”相关的陷阱,因此您的“良好代码”被严重破坏。 (2认同)