初学者的 C 输入入门入门
- 文本模式与二进制模式
- 检查fopen() 是否失败
- 陷阱
- 读取,然后解析
- 清理
文本模式与二进制模式
“二进制模式”流的读取与写入时完全相同。但是,流的末尾可能会(也可能不会)附加实现定义的空字符数('\0')。
“文本模式”流可以进行多种转换,包括(但不限于):
- 删除行尾前的空格;
- 在输出时将换行符 (
'\n') 更改为其他内容(例如,Windows 上的 "\r\n")并在输入时返回 '\n';
- 添加、更改或删除既不是打印字符(
isprint(c) 为真)、水平制表符也不是换行符的字符。
很明显,文本模式和二进制模式不能混用。以文本模式打开文本文件,以二进制模式打开二进制文件。
打开文件的尝试可能会因各种原因而失败 - 缺少权限,或者文件未找到是最常见的原因。在这种情况下,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() 会立即停止解析,而剩余的所有参数都未初始化。
- 它会告诉你它已经成功完成了多少任务——这就是为什么你应该检查它的返回码(见上文)——但不是它的确切位置停止解析输入,使得优雅的错误恢复变得困难。
- 它会跳过输入中的任何前导空格,除非它不跳过(
[、c 和 n 转换)。 (见下一段。)
- 在某些极端情况下,它的行为有些特殊。
当*scanf() 没有按预期工作时
*scanf() 的一个常见问题是输入流中存在用户未考虑的未读空格(' '、'\n'、...)。
读取数字("%d" 等)或字符串("%s")会在任何空白处停止。虽然大多数 *scanf() 转换说明符 skip 在输入中前导空格,但 [、c 和 n 不会。所以换行符仍然是第一个待处理的输入字符,导致 %c 和 %[ 无法匹配。
您可以通过显式读取来跳过输入中的换行符,例如通过fgetc(),或在*scanf() 格式字符串中添加一个空格。 (格式字符串中的单个空格与输入中的 任意 个空格匹配。)
读取,然后解析
我们只是建议不要使用*scanf(),除非您确实、肯定地知道自己在做什么。那么,用什么来代替呢?
不要像*scanf() 尝试做的那样一次性读取和解析输入,而是将这些步骤分开。
通过fgets()读取(部分)输入行
fgets() 有一个参数,用于将其输入限制为最多那么多字节,避免缓冲区溢出。如果输入行完全适合您的缓冲区,则缓冲区中的最后一个字符将是换行符 ('\n')。如果它不完全适合,您正在查看部分读取的行。
解析内存中的行
对于内存解析特别有用的是 strtol() 和 strtod() 函数系列,它们提供与 *scanf() 转换说明符 d、i、u、o、@ 类似的功能987654398@、a、e、f、g。
但他们也准确地告诉你他们停止解析的位置,并且有意义地处理对于目标类型来说太大的数字。
除此之外,C 还提供wide range of string processing functions。由于您将输入保存在内存中,并且始终知道您已经解析了多远,因此您可以多次返回以尝试理解输入。
如果所有其他方法都失败了,您可以使用整行来为用户打印有用的错误消息。
清理
确保明确关闭已(成功)打开的所有流。这会刷新所有尚未写入的缓冲区,并避免资源泄漏。
fclose(fp);