Answers:
读取输入的最常见方式是:
fgets
通常建议使用固定大小的尺寸,以及
使用fgetc
,如果您只阅读一个,可能会很有用char
。
要转换输入,可以使用多种功能:
strtoll
,将字符串转换为整数
strtof
/ d
/ld
,将字符串转换成浮点数
sscanf
,虽然它确实具有以下提到的大部分缺点,但它并不像简单地使用它那样糟糕scanf
在普通的ANSI C中,没有很好的方法来解析以分隔符分隔的输入。strtok_r
从POSIX或中使用strtok
都是不安全的。您也可以使用和滚动自己的线程安全变体,因为它不涉及任何特殊的OS支持。strcspn
strspn
strtok_r
可能有些矫kill过正,但是您可以使用词法分析器和解析器(flex
并且bison
是最常见的示例)。
无需转换,只需使用字符串
由于我没有确切说明为什么 scanf
我的问题不好,所以我将详细说明:
使用转换说明符%[...]
和%c
,scanf
不会占用空白。正如这个问题的许多重复所证明的那样,这显然不是广为人知。
关于何时&
在引用scanf
的参数(特别是字符串)时使用一元运算符存在一些困惑。
忽略的返回值非常容易scanf
。通过读取未初始化的变量,很容易导致未定义的行为。
忘记防止缓冲区溢出很容易scanf
。scanf("%s", str)
甚至与一样糟糕gets
。
使用转换整数时,您无法检测到溢出scanf
。实际上,溢出会导致这些函数发生未定义的行为。
scanf
不好?主要问题在于,scanf
它从来没有打算处理用户输入。它旨在与“完全”格式化的数据一起使用。我引用了“完全”一词,因为它不是完全正确的。但是,它并非旨在分析不像用户输入那样可靠的数据。从本质上讲,用户输入是不可预测的。用户会误解说明,打错字,在执行之前不小心按回车键等。一个人可能会合理地问,为什么读取的功能不应该用于用户输入stdin
。如果您是经验丰富的* nix用户,则说明不会感到惊讶,但可能会使Windows用户感到困惑。在* nix系统中,构建通过管道运行的程序非常普遍,stdout
stdin
第二。这样,您可以确保输出和输入是可预测的。在这些情况下,scanf
实际上效果很好。但是,当使用不可预测的输入时,您会冒各种麻烦。
那么为什么没有用于用户输入的易于使用的标准功能呢?只能在这里猜测,但是我认为老的铁杆C黑客只是认为现有功能足够好,即使它们很笨拙。另外,当您查看典型的终端应用程序时,它们很少会从中读取用户输入stdin
。通常,您会将所有用户输入作为命令行参数传递。当然,也有例外,但是对于大多数应用程序来说,用户输入是一件非常小的事情。
我最喜欢的是fgets
与结合使用sscanf
。我曾经写过一个答案,但是我将重新发布完整的代码。这是一个体面的(但不是完美的)错误检查和解析的示例。它足够用于调试。
注意
我不太喜欢让用户在一行上输入两个不同的东西。只有当它们以自然的方式相互归属时,我才这样做。例如
printf("Enter the price in the format <dollars>.<cent>: ")
,然后使用sscanf(buffer "%d.%d", &dollar, ¢)
。我永远不会做类似的事情printf("Enter height and base of the triangle: ")
。使用fgets
下面的要点是封装输入,以确保一个输入不会影响下一个。
#define bsize 100
void error_function(const char *buffer, int no_conversions) {
fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
fprintf(stderr, "%d successful conversions", no_conversions);
exit(EXIT_FAILURE);
}
char c, buffer[bsize];
int x,y;
float f, g;
int r;
printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);
// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);
// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);
printf("You entered %d %d %f %c\n", x, y, f, c);
如果您做很多这样的事情,我建议您创建一个始终刷新的包装器:
int printfflush (const char *format, ...) { va_list arg; int done; va_start (arg, format); done = vfprintf (stdout, format, arg); fflush(stdout); va_end (arg); return done; }```
这样做将消除一个常见的问题,即尾随换行符可能会影响嵌套输入。但这还有另一个问题,那就是如果行长于bsize
。您可以使用进行检查if(buffer[strlen(buffer)-1] != '\n')
。如果要删除换行符,可以使用来完成buffer[strcspn(buffer, "\n")] = 0
。
通常,我建议不要期望用户以某种奇怪的格式输入您应该解析为不同变量的输入。如果要分配变量height
和width
,请勿同时要求两个。允许用户在它们之间按Enter。同样,从某种意义上说,这种方法是很自然的。在您按stdin
回车键之前,您永远不会得到输入,那么为什么不总是读取整行呢?当然,如果行比缓冲区长,这仍然可能导致问题。我是否记得提到用户输入在C语言中比较笨拙?:)
为了避免行长超过缓冲区的问题,可以使用自动分配适当大小的缓冲区的函数,可以使用getline()
。缺点是您free
以后需要结果。
如果您真的想使用用户输入在C中创建程序,我建议您看一下类似的库ncurses
。因为那样的话,您可能还想创建带有某些终端图形的应用程序。不幸的是,如果这样做,您将失去一些可移植性,但是它可以使您更好地控制用户输入。例如,它使您能够立即读取按键,而不必等待用户按下Enter。
fgets()
of "1 2 junk"
,if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {
即使它具有“垃圾邮件” ,也不会报告输入有任何错误。
scanf
用于完美格式化的数据,但事实并非如此。除了@chux提到的“垃圾”问题外,还有一个事实是,像这样的格式"%d %d %d"
很高兴从一,两或三行(如果中间有空行,甚至更多)读取输入,则没有通过执行诸如"%d\n%d %d"
等操作 来强制(说)两行输入的方法scanf
可能适用于格式化的流输入,但对于基于行的任何内容都根本不好。
scanf
当您知道您的输入始终结构合理且行为良好时,它就很棒。除此以外...
IMO,这是最大的问题scanf
:
缓冲区溢出的风险 -如果您未为%s
和%[
指定转换符指定字段宽度,则会冒缓冲区溢出的风险(尝试读取的输入量超过缓冲区的大小)。不幸的是,没有一种很好的方法将其指定为参数(与一样printf
)-您必须将其作为转换说明符的一部分进行硬编码,或者执行一些宏的设计。
接受应该被拒绝的输入-如果您正在使用%d
转换说明符读取输入,并且输入类似的内容12w4
,那么您会希望 scanf
拒绝该输入,但不会-它将成功转换并分配12
,并留w4
在输入流中弄乱了下一次阅读。
那么,您应该使用什么呢?
我通常建议使用以下方式将所有交互式输入读取为文本fgets
-它允许您指定一次最多读取的字符数,因此可以轻松防止缓冲区溢出:
char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
// error reading from input stream, handle as appropriate
}
else
{
// process input buffer
}
一个怪癖fgets
是,如果有空的话,它将尾随换行符存储在缓冲区中,因此您可以轻松检查一下,是否有人输入的输入超出您的预期:
char *newline = strchr( input, '\n' );
if ( !newline )
{
// input longer than we expected
}
如何处理取决于您自己-您可以立即拒绝全部输入,也可以使用以下命令清除所有剩余输入getchar
:
while ( getchar() != '\n' )
; // empty loop
或者,您可以处理到目前为止所获得的输入,然后再次阅读。这取决于您要解决的问题。
要标记输入(基于一个或多个定界符将其分割),可以使用strtok
,但要注意- strtok
修改其输入(它使用字符串终止符覆盖定界符),并且您无法保留其状态(即,您可以t对一个字符串进行部分标记,然后开始对另一个字符串进行标记,然后从原始字符串中停下来的地方开始。有一个变体,strtok_s
可以保留令牌生成器的状态,但是AFAIK的实现是可选的(您需要检查__STDC_LIB_EXT1__
已定义的定义,以查看其是否可用)。
对输入进行标记后,如果需要将字符串转换为数字(即"1234"
=> 1234
),则可以选择。 strtol
并将strtod
整数和实数的字符串表示形式转换为它们各自的类型。它们还使您能够12w4
解决上面提到的问题-它们的一个参数是指向未在字符串中转换的第一个字符的指针:
char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
// input is not a valid integer string, reject the entire input
else
val = tmp;
%*[%\n]
,对于以后在答案中处理超长行很有用)。
snprintf()
),。
isspace()
-它接受以表示的未签名字符int
,因此您需要进行强制转换unsigned char
以避免在char
已签名的平台上使用UB 。
在这个答案中,我将假设您正在阅读和解释文本行。也许您是在提示用户,该用户正在输入内容并单击RETURN。也许您正在从某种数据文件中读取结构化文本行。
由于您正在阅读文本行,因此围绕读取一行文本的库函数来组织代码是很有意义的。标准功能是fgets()
,尽管还有其他功能(包括getline
)。然后,下一步就是以某种方式解释该行文本。
这是调用fgets
以读取一行文本的基本方法:
char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);
这只是读入一行文本并将其打印出来。如所写,它有一些限制,我们将在稍后讨论。它还具有一个非常好的功能:作为第二个参数传递给我们的数字512 fgets
是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
声明的位置。这很麻烦,因此有两种更好的方法来保持大小同步。您可以,(a)使用预处理器为尺寸命名:
#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
脱掉后背。我承认,这确实是一件令人讨厌的事。我希望有一个Standard函数,我可以为您指出这个小问题。(请没有人提出gets
。)但是相比于scanf's
17种不同的烦恼,我fgets
每天都会采取这种烦恼。
那么如何做你带的是换行?三种方式:
(a)明显的方式:
char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';
(b)狡猾而紧凑的方式:
strtok(line, "\n");
不幸的是,这并不总是可行。
(c)另一种紧凑而又模糊的方式:
line[strcspn(line, "\n")] = '\0';
而现在,这是的出路,我们可以回到另一件事我跳过了:的不完善atoi()
和atof()
。这些问题是,它们没有给您成功或失败成功的任何有用指示:它们静默地忽略尾随的非数字输入,并且如果根本没有数字输入,它们静默地返回0。首选的替代方法-也具有某些其他优点-是strtol
和strtod
。
strtol
还可以让您使用10以外的底数,这意味着您可以(除其他外)获得%o
或%x
与scanf
。但是,展示如何正确使用这些功能本身就是一个故事,对于已经变成零碎的叙述来说,这会太分心了,因此,我现在不再赘述。
您可能想解析的其余主要叙述性问题输入内容比单个数字或字符还要复杂。如果您想读取包含两个数字,多个空格分隔的单词或特定的框架标点符号的行,该怎么办?那就是事情变得有趣的地方,如果您尝试使用做事情的地方,事情可能会变得复杂scanf
,并且既然您已经使用干净地阅读了一行文本,那么这里还有更多的选择fgets
,尽管所有这些选择的全部内容可能会填满一本书,所以我们只能在这里刮擦表面。
我最喜欢的技术是将行划分为用空格分隔的“单词”,然后对每个“单词”做进一步的处理。这样做的一个主要标准功能是
strtok
(它也有其问题,并且还对整个单独的讨论进行评分)。我自己的喜好是专用功能,用于构造指向每个分开的“单词”的指针的数组,这是我在本课程笔记中描述的功能
。无论如何,一旦你有“字”,你可以进一步处理每一个,也许与同atoi
/ atof
/ strtol
/ strtod
我们已经看过的功能。
矛盾的是,尽管我们在这里花了很多时间和精力来弄清楚如何远离它scanf
,但是处理刚刚阅读的文本行的另一种好方法
fgets
是将其传递给sscanf
。这样,您将获得的大多数优点scanf
,而没有大多数缺点。
如果您的输入语法特别复杂,则可能适合使用“ regexp”库进行解析。
最后,您可以使用任何适合您的临时解析解决方案。您可以通过char *
指针一次检查一行字符,以
检查所需字符。或者,您可以使用strchr
或strrchr
或strspn
或strcspn
或来搜索特定字符strpbrk
。或者,您可以使用之前跳过的strtol
或
strtod
函数来解析/转换和跳过数字字符组。
显然还有更多可以说的,但希望本入门会帮助您入门。
sizeof (line)
而不是简单地写作sizeof line
?前者使它看起来像是line
一个类型名称!
sscanf
用作转换引擎,但使用其他工具收集(并可能按摩)输入。但也许值得一提getline
。
fscanf
的实际滋扰”时,您的意思是fgets
?令人讨厌的#3确实让我感到烦恼,特别是考虑到它scanf
返回了一个指向缓冲区的无用指针,而不是返回输入的字符数(这将使换行符的剥离更加清晰)。
sizeof
风格的解释。对我来说,记住您的父母很容易:我认为这就(type)
像是没有价值的演员(因为我们只对类型感兴趣)。另一件事:您说这strtok(line, "\n")
并不总是有效,但有时可能不明显。我猜您正在考虑行比缓冲区长的情况,因此我们没有换行符,并且strtok()
返回null?真可惜fgets()
没有返回更有用的值,所以我们可以知道换行符是否存在。
我可以用什么来解析输入而不是scanf?
相反scanf(some_format, ...)
,考虑fgets()
与sscanf(buffer, some_format_and %n, ...)
通过使用" %n"
,代码可以简单地检测是否所有格式都已成功扫描,并且末尾没有多余的非空白垃圾。
// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2]; // Suggest 2x, no real need to be stingy.
if (fgets(buffer, sizeof buffer, stdin)) {
int n = 0;
// add -------------> " %n"
sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
// Did scan complete, and to the end?
if (n > 0 && buffer[n] == '\0') {
// success, use `some_int, some_float`
} else {
; // Report bad input and handle desired.
}
让我们将解析的要求陈述为:
必须接受有效的输入(并将其转换为其他形式)
无效的输入必须被拒绝
当任何输入被拒绝时,有必要向用户提供描述性消息,说明该消息被拒绝的原因(以清晰的“非程序员的普通人容易理解的语言”),以解释为什么输入被拒绝。问题)
为了使事情变得非常简单,请考虑解析一个简单的十进制整数(由用户输入),然后再解析其他内容。拒绝用户输入的可能原因有:
我们还要正确定义“输入中包含不可接受的字符”;并说:
由此可以确定是否需要以下错误消息:
从这一点上我们可以看到,将字符串转换为整数的合适函数需要区分非常不同的错误类型。并且“ scanf()
”,“ atoi()
”或“ strtoll()
”之类的东西是完全毫无价值的,因为它们无法给您任何输入错误的提示(并且对“有效/无效”使用完全不相关和不适当的定义“输入”)。
相反,让我们开始写一些没用的东西:
char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
return "Code not implemented yet!";
}
int main(int argc, char *argv[]) {
char *errorString;
int value;
if(argc < 2) {
printf("ERROR: No command line argument.\n");
return EXIT_FAILURE;
}
errorString = convertStringToInteger(&value, argv[1], -10, 2000);
if(errorString != NULL) {
printf("ERROR: %s\n", errorString);
return EXIT_FAILURE;
}
printf("SUCCESS: Your number is %d\n", value);
return EXIT_SUCCESS;
}
满足规定的要求;此convertStringToInteger()
功能本身可能最终会成为数百行代码。
现在,这只是“解析一个简单的十进制整数”。想象一下,如果您想解析复杂的事物;例如“姓名,街道地址,电话号码,电子邮件地址”结构的列表;或者像编程语言一样 对于这些情况,您可能需要编写成千上万的代码来创建一个不是残酷的笑话的解析。
换一种说法...
我可以用什么来解析输入而不是scanf?
自己编写(可能数千行)代码,以满足您的要求。
这是一个flex
用于扫描简单输入的示例,在这种情况下,该文件是ASCII浮点数文件,该文件可能采用US(n,nnn.dd
)或European(n.nnn,dd
)格式。这只是从更大的程序中复制的,因此可能存在一些未解决的引用:
/* This scanner reads a file of numbers, expecting one number per line. It */
/* allows for the use of European-style comma as decimal point. */
%{
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifdef WINDOWS
#include <io.h>
#endif
#include "Point.h"
#define YY_NO_UNPUT
#define YY_DECL int f_lex (double *val)
double atofEuro (char *);
%}
%option prefix="f_"
%option nounput
%option noinput
EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS [ \t\x0d]
%%
[!@#%&*/].*\n
^{WS}*{EURONUM}{WS}* { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}* { *val = atof (yytext); return (1); }
[\n]
.
%%
/*------------------------------------------------------------------------*/
int scan_f (FILE *in, double *vals, int max)
{
double *val;
int npts, rc;
f_in = in;
val = vals;
npts = 0;
while (npts < max)
{
rc = f_lex (val);
if (rc == 0)
break;
npts++;
val++;
}
return (npts);
}
/*------------------------------------------------------------------------*/
int f_wrap ()
{
return (1);
}
其他答案给出了正确的低级详细信息,因此,我将自己限制在较高级别:首先,分析您希望每条输入行的外观。尝试用形式上的语法描述输入-运气不错,您会发现可以使用常规语法或至少使用上下文无关的语法来描述输入。如果常规语法足够,那么您可以编写有限状态机可以一次识别并解释每个命令行一个字符。然后,您的代码将读取一行(如其他答复中所述),然后通过状态机扫描缓冲区中的字符。在某些状态下,您停止并将到目前为止已扫描的子字符串转换为一个数字或其他。如果就这么简单,您可能可以“自己动手”;如果您发现您需要完整的无上下文语法,那么最好弄清楚如何使用现有的解析工具(re:lex
和/ yacc
或其变体)。
errno == EOVERFLOW
使用后检查strtoll
)。
(r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2
尾随非数字文本不会检测为不良。