在这个答案中,我假设你正在阅读和解释文本行。
也许你正在提示用户,他正在输入一些内容并按下RETURN键。或者你正在从某种数据文件中读取结构化文本行。
既然你正在读取文本行,那么围绕一个读取文本行的库函数组织你的代码是有意义的。
标准函数是fgets()
,虽然还有其他函数(包括getline
)。然后下一步是以某种方式解释该文本行。
以下是调用fgets
读取文本行的基本步骤:
char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);
这段代码简单地读取一行文本并将其打印出来。但是,它有几个限制,我们稍后会讨论到。它还有一个非常好的特性:我们传递给fgets
作为第二个参数的数字512是数组line
的大小,我们要求fgets
读入。这个事实——我们可以告诉fgets
它被允许读取多少——意味着我们可以确保fgets
不会通过读取太多内容而溢出数组。
现在我们知道如何读取一行文本,但如果我们真的想读取一个整数、一个浮点数、一个单字符或一个单词怎么办?(也就是说,如果我们试图改进的scanf
调用使用了像%d
、%f
、%c
或%s
这样的格式说明符呢?)
将一行文本 - 字符串 - 重新解释为其中的任何一种形式非常容易。要将字符串转换为整数,最简单(尽管不完美)的方法是调用atoi()
。要转换为浮点数,有atof()
。(我们很快会看到更好的方法)。这里是一个非常简单的例子:
printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);
如果您希望用户输入单个字符(例如作为是/否回答的
y
或
n
),您可以直接获取该行的第一个字符,如下所示:
printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);
当然,这忽略了用户可能输入多个字符的情况;它会静默地忽略任何额外输入的字符。
最后,如果您希望用户输入的字符串绝对不包含空格,如果您想要处理输入行
hello world!
作为字符串"hello"
后面跟着其他内容(这就是scanf
格式%s
所做的),好吧,在这种情况下,我有点说谎了,重新解释这一行并不那么容易,因此对于问题的这部分答案将需要等待一段时间。
但首先,我想回到我跳过的三件事情。
(1) 我们一直在称呼
fgets(line, 512, stdin);
为了读取到数组line
中,而512是数组line
的大小,所以fgets
知道不要溢出。但是为了确保512是正确的数字(特别是检查是否有人调整了程序来更改大小),您必须回到声明line
的位置。这很麻烦,因此有两种更好的方法来保持大小同步。
您可以使用预处理器来为大小命名:
#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);
或者,(b)使用C语言的
sizeof
运算符:
fgets(line, sizeof(line), stdin);
(2)第二个问题是我们没有检查错误。当您读取输入时,应始终检查可能出现的错误。如果由于任何原因fgets无法读取您要求的文本行,则通过返回空指针来指示此情况。因此,我们应该做一些像这样的事情:
printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
printf("Well, never mind, then.\n");
exit(1);
}
最后,还有一个问题,为了读取一行文本,
fgets
会读取字符并将它们填充到数组中,直到找到终止该行的
\n
字符,并且它也会将
\n
字符填充到数组中。如果您稍微修改我们之前的示例,就可以看到这一点:
printf("you typed: \"%s\"\n", line);
如果我运行这个程序并在提示时输入“Steve”,它会打印出:
you typed: "Steve
"
第二行上的那个 "
是因为它读取并打印回去的字符串实际上是 "Steve\n"
。
有时候这多余的换行符并不重要(比如当我们调用 atoi
或 atof
时,因为它们都会忽略数字之后的任何非数字输入),但有时候它很重要。所以我们经常需要去掉这个换行符。有几种方法可以做到这一点,我等一下再说。(我知道我一直在说这些话,但我保证我会回到所有这些问题上)。
此时,你可能会想:"我以为你说 scanf
不好用,而另一种方式会更好。但是 fgets
开始看起来像一个麻烦的东西。调用 scanf
是如此简单!我不能继续使用它吗?"
当然,如果你愿意的话,可以继续使用scanf
。(对于非常简单的事情来说,在某些方面它确实更简单。)但是,请不要在它因为其17个怪癖和缺陷之一而失败时向我哭诉,或者因为输入了你没有预料到的内容而进入无限循环,或者当你无法弄清如何使用它来完成更复杂的任务时向我求助。现在让我们来看看fgets
的实际麻烦:
你总是需要指定数组大小。当然,这一点一点也不麻烦 - 这是一个特性,因为缓冲区溢出是非常糟糕的事情。
你必须检查返回值。实际上,这没有什么区别,因为要正确使用scanf,您也必须检查它的返回值。
你必须将\n删除。我承认,这是真正的麻烦。我希望有一个标准函数,我可以指向它而没有这个小问题。(请不要提gets)。但与scanf的17种不同烦恼相比,我每天都会选择fgets的这一个烦恼。
那么你如何删除这个换行符?有许多方法:
(a)显而易见的方法:
char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';
(b)巧妙且紧凑的方式:
strtok(line, "\n");
不幸的是,这个在空行上无法正常工作。
(c) 另一种紧凑且稍微晦涩的方式:
line[strcspn(line, "\n")] = '\0';
还有其他的方法。我个人总是使用(a),因为它简单明了,虽然不够简洁。
请参见this question或this question,以获取更多关于从fgets
中去除\n
的信息。
现在这个问题解决了,我们可以回到另一个我之前跳过的问题:
atoi()
和
atof()
的缺陷。这些函数的问题在于它们不会给你任何有用的成功或失败指示:它们会静默地忽略尾随的非数字输入,并且如果根本没有数字输入,它们会静默地返回 0。首选的替代方案 - 还具有某些其他优点 - 是
strtol
和
strtod
。
strtol
还允许您使用除 10 以外的基数,这意味着您可以使用
scanf
的效果(包括但不限于)
%o
或
%x
。但是,正确使用这些函数的方法已经是一个独立的故事,而且会分散注意力,所以我现在不会再多说什么关于它们的内容了。
主要叙述的其余部分涉及您可能正在尝试解析的输入,这些输入比单个数字或字符更复杂。如果您想读取包含两个数字、多个以空格分隔的单词或特定框架标点符号的行,该怎么办?那就很有趣了,如果您尝试使用scanf
进行操作,事情可能会变得非常复杂,而且现在您已经使用fgets
清晰地读取了一行文本,因此选项远远超过了以前,尽管所有这些选项的完整故事可能填满一本书,但我们只能在这里浅尝辄止。
我最喜欢的技巧是将文本行分解成由空格分隔的“单词”,然后对每个“单词”进行进一步处理。一个主要的标准函数用于此操作是strtok
(它也有其问题,并且需要单独讨论)。我自己更喜欢使用专门的函数来构建指向每个拆分“单词”的指针数组,这个函数我在这些课程笔记中描述了。无论如何,一旦你获得了“单词”,你可以进一步处理每个“单词”,也许使用我们已经看过的相同的atoi
/atof
/strtol
/strtod
函数。
矛盾的是,尽管我们在这里花费了相当多的时间和精力来摆脱scanf
,但另一种处理刚刚用fgets
读取的文本行的好方法是将其传递给sscanf
。通过这种方式,您可以获得大部分scanf
的优点,但没有大部分缺点。
如果您的输入语法特别复杂,则可能适合使用“正则表达式”库进行解析。
最后,您可以使用任何适合您的特定解析解决方案。您可以使用char *
指针逐个字符移动文本行,并检查您期望的字符。或者,您可以使用像strchr
或strrchr
、strspn
或strcspn
、strpbrk
这样的函数搜索特定字符。或者,您可以使用我们之前跳过的strtol
或strtod
函数解析/转换并跳过一组数字字符。
当然还有很多可以说的,但是希望这个介绍能够让你入门。
scanf
不会吃掉空格。" 可以翻译为 "scanf
不会吃掉可选的前导空格。"%[...]
和%c
都可以轻松读取空格,但也许不是程序员想要的方式。 - chux - Reinstate Monica