我可以用什么代替scanf进行输入转换?

JL2*_*210 124 c scanf

我经常看到人们不鼓励其他人使用scanf并说有更好的选择。但是,我最终看到的只是“不要使用scanf“这里是正确的格式字符串”,并且从来没有提到“更好的替代方案”的任何示例。

例如,让我们看一下这段代码:

scanf("%c", &c);
Run Code Online (Sandbox Code Playgroud)

这将读取最后一次转换后留在输入流中的空白。通常建议的解决方案是使用:

scanf(" %c", &c);
Run Code Online (Sandbox Code Playgroud)

还是不使用scanf

由于scanf不好,用于转换scanf通常无需使用即可处理的输入格式(例如整数,浮点数和字符串)的ANSI C选项有哪些scanf

JL2*_*210 81

读取输入的最常见方式是:

  • fgets通常建议使用固定大小的尺寸,以及

  • 使用fgetc,如果您只阅读一个,可能会很有用char

要转换输入,可以使用多种功能:

  • strtoll,将字符串转换为整数

  • strtof/ d/ ld,将字符串转换成浮点数

  • sscanf,虽然它确实具有以下提到的大部分缺点,但它并不像简单地使用它那样糟糕scanf

  • 没有很好的方法来解析普通ANSI C中以分隔符分隔的输入。可以strtok_r从POSIX或strtok,这都不是线程安全的。您也可以使用和滚动自己的线程安全变体,因为它不涉及任何特殊的OS支持。strcspnstrspnstrtok_r

  • 可能有些矫kill过正,但是您可以使用词法分析器和解析器(flex并且bison是最常见的示例)。

  • 无需转换,只需使用字符串


既然您没有确切说明 scanf问题中为什么不好,所以我将详细说明:

  • 使用转换说明符%[...]%cscanf不会占用空白。正如这个问题的许多重复所证明的那样,这显然不是广为人知。

  • 关于何时&在引用scanf的参数(特别是字符串)时使用一元运算符存在一些困惑。

  • 忽略的返回值非常容易scanf。通过读取未初始化的变量,很容易导致未定义的行为。

  • 忘记防止缓冲区溢出很容易scanfscanf("%s", str)甚至与一样糟糕gets

  • 使用转换整数时,您无法检测到溢出scanf实际上,溢出会导致这些函数发生未定义的行为



klu*_*utt 53

为什么scanf不好?

主要问题在于,scanf它从来没有打算处理用户输入。它旨在与“完全”格式化的数据一起使用。我引用了“完全”一词,因为它不是完全正确的。但是,它并非旨在分析不像用户输入那样可靠的数据。从本质上讲,用户输入是不可预测的。用户会误解说明,打错字,在执行之前不小心按Enter等。人们可能会合理地问为什么从中读取不应用于用户输入的功能stdin。如果您是经验丰富的* nix用户,则说明不会感到惊讶,但可能会使Windows用户感到困惑。在* nix系统中,构建通过管道运行的程序非常普遍,stdoutstdin第二。这样,您可以确保输出和输入是可预测的。在这些情况下,scanf实际上效果很好。但是,当使用不可预测的输入时,您会冒各种麻烦。

那么为什么没有用于用户输入的易于使用的标准功能呢?一个人只能在这里猜测,但是我认为,即使是笨拙的老顽固的C黑客,他们也以为现有功能足够好。另外,当您查看典型的终端应用程序时,它们很少会从中读取用户输入stdin。通常,您会将所有用户输入作为命令行参数传递。当然,也有例外,但是对于大多数应用程序来说,用户输入是一件非常小的事情。

所以,你可以做什么?

我最喜欢的是fgets与结合使用sscanf。我曾经写过一个答案,但是我将重新发布完整的代码。这是一个体面的(但不是完美的)错误检查和解析的示例。它足够用于调试。

注意

我不太喜欢让用户在一行上输入两个不同的东西。只有当它们以自然的方式相互归属时,我才这样做。例如printf("Enter the price in the format <dollars>.<cent>: "),然后使用sscanf(buffer "%d.%d", &dollar, &cent)。我永远不会做类似的事情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);
Run Code Online (Sandbox Code Playgroud)

这样做将消除一个常见的问题,即尾随换行符可能会影响嵌套输入。但这还有另一个问题,那就是如果行长于bsize。您可以使用进行检查if(buffer[strlen(buffer)-1] != '\n')。如果要删除换行符,可以使用来完成buffer[strcspn(buffer, "\n")] = 0

通常,我建议不要期望用户以某种奇怪的格式输入您应该解析为不同变量的输入。如果要分配变量heightwidth,请勿同时要求两个。允许用户在它们之间按Enter。同样,从某种意义上说,这种方法是很自然的。在您按stdin回车键之前,您永远不会得到输入,那么为什么不总是读取整行呢?当然,如果行比缓冲区长,这仍然可能导致问题。我是否记得提到用户输入在C语言中比较笨拙?:)

为了避免行长于缓冲区的问题,可以使用自动分配适当大小的缓冲区的函数,可以使用getline()。缺点是您free以后需要结果。

加强比赛

如果您真的想使用用户输入在C中创建程序,我建议您看一下类似的库ncurses。因为那样的话,您可能还想创建带有某些终端图形的应用程序。不幸的是,如果这样做,您将失去一些可移植性,但是它可以使您更好地控制用户输入。例如,它使您能够立即读取按键,而不必等待用户按下Enter。

  • *`scanf` 旨在与完美格式化的数据一起使用*但即使这样也不是真的。除了 @chux 提到的“垃圾”问题之外,还有一个事实是,像“%d %d %d”这样的格式很乐意读取一行、两行或三行(甚至更多行,如果有中间的空白行),没有办法通过执行类似“%d\n%d %d”等操作来强制(比如说)两行输入。“scanf”可能适合格式化的 *stream * 输入,但它对于基于行的任何东西都没有好处。 (2认同)

Joh*_*ode 16

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
}
Run Code Online (Sandbox Code Playgroud)

一个怪癖fgets是,如果有空的话,它将尾随换行符存储在缓冲区中,因此您可以轻松检查一下,是否有人输入的输入超出您的预期:

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}
Run Code Online (Sandbox Code Playgroud)

如何处理取决于您自己-您可以立即拒绝全部输入,也可以使用以下命令清除所有剩余输入getchar

while ( getchar() != '\n' ) 
  ; // empty loop
Run Code Online (Sandbox Code Playgroud)

或者,您可以处理到目前为止所输入的内容,然后重新阅读。这取决于您要解决的问题。

标记输入(基于一个或多个定界符将其分割),可以使用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;
Run Code Online (Sandbox Code Playgroud)

  • 您在其中使用isspace()犯了最常见的错误-它接受表示为`int`的* unsigned *字符,因此您需要转换为`unsigned char`以避免在已对`char`进行签名的平台上使用UB。 (5认同)

Ste*_*mit 7

在这个答案中,我将假设您正在阅读和解释文本行。也许您是在提示用户,该用户正在输入内容并单击RETURN。也许您正在从某种数据文件中读取结构化文本行。

由于您正在阅读文本行,因此围绕读取一行文本的库函数来组织代码是很有意义的。标准功能是fgets(),尽管还有其他功能(包括getline)。然后,下一步就是以某种方式解释该行文本。

这是调用fgets以读取一行文本的基本方法:

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);
Run Code Online (Sandbox Code Playgroud)

这只是读入一行文本并将其打印出来。如所写,它有一些限制,我们将在稍后讨论。它还具有一个非常好的功能:作为第二个参数传递给我们的数字512 是要读取fgets的数组的大小 。这个事实-我们可以知道允许读取的数量-意味着我们可以确保不会对数组进行过多读取而使数组溢出。linefgetsfgetsfgets

因此,现在我们知道如何读取一行文本,但是如果我们真的想读取整数,浮点数,单个字符或单个单词怎么办?(也就是说,如果什么 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);
Run Code Online (Sandbox Code Playgroud)

如果您希望用户键入单个字符(例如,y或者 n作为是/否响应),则可以从字面上抓取该行的第一个字符,如下所示:

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);
Run Code Online (Sandbox Code Playgroud)

(当然,这忽略了用户键入多字符响应的可能性;它悄悄地忽略了键入的任何其他字符。)

最后,如果您想让用户键入一个绝对包含空格的字符串,如果您想处理输入行

hello world!
Run Code Online (Sandbox Code Playgroud)

因为字符串"hello"后面跟着其他东西(这是scanf格式%s所要完成的),那么,在那种情况下,我花了些力气,毕竟以这种方式重新解释该行并不是那么容易问题的一部分将不得不等待一段时间。

但是首先我想回到我跳过的三件事。

(1)我们一直在打电话

fgets(line, 512, stdin);
Run Code Online (Sandbox Code Playgroud)

读取数组line,其中512是数组的大小,line因此fgets知道不会溢出它。但是要确保512是正确的数字(特别是要检查是否有人对程序进行了调整以更改大小),则必须将其读回line声明的位置。这很麻烦,因此有两种更好的方法来保持大小同步。(a)使用预处理器为尺寸命名:

#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);
Run Code Online (Sandbox Code Playgroud)

或者,(b)使用C的sizeof运算符:

fgets(line, sizeof(line), stdin);
Run Code Online (Sandbox Code Playgroud)

(2)第二个问题是我们没有检查错误。读取输入时,应始终检查是否存在错误。如果出于某种原因fgets无法读取您要求的文本行,则通过返回空指针来表明这一点。所以我们应该做类似的事情

printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
    printf("Well, never mind, then.\n");
    exit(1);
}
Run Code Online (Sandbox Code Playgroud)

最后,还有问题,为了读文本行, fgets读取字符,并将其填充到您的阵列,直到它找到\n终止行字符,并且它填充\n字符到您的数组,太。如果您稍微修改我们前面的示例,可以看到以下内容:

printf("you typed: \"%s\"\n", line);
Run Code Online (Sandbox Code Playgroud)

如果我运行此程序并在提示我时键入“ Steve”,则会打印出

you typed: "Steve
"
Run Code Online (Sandbox Code Playgroud)

"在第二行是因为字符串它读取和打印退了出去竟是"Steve\n"

有时,多余的换行符无关紧要(例如当我们调用 atoi或时atof,因为它们都忽略了数字后的任何其他非数字输入),但有时却很重要。因此,通常我们希望剥离该换行符。有几种方法可以做到,我将在稍后介绍。(我知道我一直在说很多。但是,我保证,我会回到所有这些事情上来的。)

在这一点上,您可能会想:“我以为您说的scanf 不好,否则这会更好。但是fgets开始看起来很麻烦。打电话scanf如此简单!我不能继续使用它吗? ”

当然,scanf如果需要,您可以继续使用。(而且对于非常 简单的事情,从某些方面来说,它更简单。)但是,请不要因为它的17个怪癖和缺点之一使它失败而哭泣,或者由于您输入而陷入无限循环没想到,或者当您不知道如何使用它来做更复杂的事情时。让我们看一下fgets的实际麻烦:

  1. 您始终必须指定数组大小。好吧,当然,这一点都不令人讨厌-这是一个功能,因为缓冲区溢出是一件很糟糕的事情。

  2. 您必须检查返回值。实际上,这很容易,因为要scanf正确使用它,您还必须检查其返回值。

  3. 您必须\n脱掉后背。我承认,这确实是一件令人讨厌的事。我希望有一个Standard函数,我可以为您指出这个小问题。(请没有人提出gets。)但是相比于scanf's17种不同的烦恼,我fgets每天都会采取这种烦恼。

那么如何你带的是换行?三种方式:

(a)明显的方式:

char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';
Run Code Online (Sandbox Code Playgroud)

(b)狡猾而紧凑的方式:

strtok(line, "\n");
Run Code Online (Sandbox Code Playgroud)

不幸的是,这并不总是可行。

(c)另一种紧凑而又模糊的方式:

line[strcspn(line, "\n")] = '\0';
Run Code Online (Sandbox Code Playgroud)

既然这已经不可行了,我们可以回到另一件事,我跳过了atoi()and 的缺点atof()。这些问题是,它们没有给您成功或失败成功的任何有用指示:它们静默地忽略尾随的非数字输入,并且如果根本没有数字输入,它们静默地返回0。首选的替代方法-也具有某些其他优点-是strtolstrtodstrtol还可以让您使用10以外的底数,这意味着您可以(除其他外)获得%o%xscanf。但是,展示如何正确使用这些功能本身就是一个故事,并且与已经变成零碎的叙述的内容分心,因此,我现在不再赘述。

您可能想解析的其余主要叙述性问题输入内容比单个数字或字符还要复杂。如果您想读取包含两个数字,多个空格分隔的单词或特定的框架标点符号的行,该怎么办?那就是事情变得有趣的地方,如果您尝试使用做事情的地方事情可能会变得复杂scanf,并且既然您已经使用干净地阅读了一行文本,那么这里还有更多的选择fgets,尽管所有这些选择的全部内容可能会填满一本书,所以我们只能在这里刮擦表面。

  1. 我最喜欢的技术是将行划分为用空格分隔的“单词”,然后对每个“单词”做进一步的处理。这样做的一个主要标准功能是 strtok(它也有其问题,并且还对整个单独的讨论进行评分)。我自己的喜好是专用功能,用于构造指向每个分开的“单词”的指针的数组,这是我在本课程笔记中描述的功能 。无论如何,一旦你有“字”,你可以进一步处理每一个,也许与同atoi/ atof/ strtol/ strtod 我们已经看过的功能。

  2. 矛盾的是,即使我们在这里花了大量的时间和精力来弄清楚如何远离它scanf,处理刚刚阅读的文本行的另一种好方法 fgets是将其传递给sscanf。这样,您将获得的大多数优点scanf,而没有大多数缺点。

  3. 如果您的输入语法特别复杂,则可能适合使用“ regexp”库进行解析。

  4. 最后,您可以使用任何适合您的临时解析解决方案。您可以通过char *指针一次检查一行字符,以 检查所需字符。或者,您可以使用strchrstrrchrstrspnstrcspn或来搜索特定字符strpbrk。或者,您可以使用之前跳过的strtolstrtod函数来解析/转换并跳过数字字符组。

显然还有更多可以说的,但希望本入门会帮助您入门。


chu*_*ica 6

我可以用什么来解析输入而不是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.
  }
Run Code Online (Sandbox Code Playgroud)


Bre*_*dan 5

让我们将解析的要求陈述为:

  • 必须接受有效的输入(并将其转换为其他形式)

  • 无效的输入必须被拒绝

  • 当任何输入被拒绝时,有必要向用户提供描述性消息,说明该消息被拒绝的原因(以“程序员以外的普通人很容易理解”的语言),以解释为什么输入被拒绝(以便人们找出解决方法)问题)

为了使事情变得非常简单,让我们考虑解析一个简单的十进制整数(由用户输入),而不解析其他任何东西。拒绝用户输入的可能原因有:

  • 输入包含不可接受的字符
  • 输入的数字小于可接受的最小值
  • 输入的数字大于可接受的最大值
  • 输入表示具有非零小数部分的数字

我们还要正确定义“输入中包含不可接受的字符”;并说:

  • 前导空格和尾随空格将被忽略(例如,“
    5”将被视为“ 5”)
  • 允许零或一个小数点(例如,“ 1234。”和“ 1234.000”都与“ 1234”相同)
  • 至少必须有一位数字(例如“。”被拒绝)
  • 小数点不能超过一个(例如,“ 1.2.3”被拒绝)
  • 不在数字之间的逗号将被拒绝(例如,“,1234”被拒绝)
  • 小数点后的逗号将被拒绝(例如,“ 1234.000,000”被拒绝)
  • 另一个逗号后的逗号被拒绝(例如,“ 1,234”被拒绝)
  • 其他所有逗号将被忽略(例如,“ 1,234”将被视为“ 1234”)
  • 拒绝不是第一个非空白字符的减号
  • 不是第一个非空白字符的正号将被拒绝

由此可以确定是否需要以下错误消息:

  • “输入开始时未知字符”
  • “输入末尾的未知字符”
  • “输入中间的未知字符”
  • “数字太低(最小值为...。)”
  • “数量太高(最大为...。”)
  • “数字不是整数”
  • “小数点太多”
  • “没有小数位数”
  • “数字开头的逗号逗号”
  • “末尾的逗号逗号”
  • “数字中间的逗号逗号”
  • “小数点后的逗号逗号”

从这一点上我们可以看出,将字符串转换为整数的合适函数将需要区分非常不同的错误类型。并且“ 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;
}
Run Code Online (Sandbox Code Playgroud)

满足规定的要求;此convertStringToInteger()功能本身可能最终会成为数百行代码。

现在,这只是“解析一个简单的十进制整数”。想象一下,如果您想解析复杂的事物;例如“姓名,街道地址,电话号码,电子邮件地址”结构的列表;或者像编程语言一样 对于这些情况,您可能需要编写成千上万的代码来创建一个不是残酷的笑话的解析。

换一种说法...

我可以用什么来解析输入而不是scanf?

自己编写(可能数千行)代码,以满足您的要求。


jam*_*sqf 5

这是一个flex用于扫描简单输入的示例,在本例中是一个 ASCII 浮点数文件,可能是美国 ( n,nnn.dd) 或欧洲 ( 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);
}
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

8381 次

最近记录:

5 年,11 月 前