【问题标题】:What can I use for input conversion instead of scanf?我可以使用什么来进行输入转换而不是 scanf?
【发布时间】:2020-02-12 15:47:27
【问题描述】:

我经常看到有人劝阻其他人不要使用scanf,并说有更好的选择。但是,我最终看到的只是 “不要使用 scanf“这是一个正确的格式字符串”,而且从来没有任何 示例提到了“更好的选择”

例如,让我们以这段代码的 sn-p 为例:

scanf("%c", &c);

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

scanf(" %c", &c);

或者不使用scanf

由于scanf 不好,有哪些ANSI C 选项可以转换scanf 通常可以在不使用scanf 的情况下处理的输入格式(例如整数、浮点数和字符串)?

【问题讨论】:

    标签: c scanf


    【解决方案1】:

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

    • 使用固定大小的fgets,这是通常建议的,并且

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

    要转换输入,您可以使用多种函数:

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

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

    • sscanf,虽然它确实有下面提到的大部分缺点

    • 在普通的 ANSI C 中没有解析分隔符分隔输入的好方法。要么使用来自 POSIX 的 strtok_r,要么使用 strtok,这不是线程安全的。您也可以使用strcspnstrspn 来实现roll your own 线程安全变体,因为strtok_r 不涉及任何特殊的操作系统支持。

    • 这可能有点矫枉过正,但您可以使用词法分析器和解析器(flexbison 是最常见的例子)。

    • 不用转换,直接用字符串


    由于我没有详细说明为什么 scanf 在我的问题中不好,我会详细说明:

    • 使用转换说明符 %[...]%cscanf 不会占用空格。这显然并不广为人知,this question 的许多重复就是证明。

    • 在引用 scanf 的参数(特别是字符串)时,何时使用一元 & 运算符存在一些混淆。

    • 很容易忽略来自scanf 的返回值。这很容易导致读取未初始化变量的未定义行为。

    • scanf 中很容易忘记防止缓冲区溢出。 scanf("%s", str)gets 一样糟糕,甚至比gets 更糟糕。

    • 使用scanf 转换整数时无法检测到溢出。 实际上,溢出会导致这些函数中的undefined behavior


    【讨论】:

      【解决方案2】:

      TL;DR

      fgets 用于获取输入。 sscanf 用于事后解析。 scanf 试图同时做这两个。这是麻烦的秘诀。先读后解析。

      为什么scanf 不好?

      主要问题是scanf 从未打算处理用户输入。它旨在与“完美”格式化的数据一起使用。我引用了“完美”这个词,因为它并不完全正确。但它并非旨在解析与用户输入一样不可靠的数据。本质上,用户输入是不可预测的。用户误解了指令、打错字、在完成之前不小心按了 Enter 键等等。人们可能会合理地问为什么不应该用于用户输入的函数读取自 stdin。如果您是一位经验丰富的 *nix 用户,那么这个解释不会让人感到意外,但它可能会让 Windows 用户感到困惑。在 *nix 系统中,构建通过管道运行的程序是很常见的,这意味着您通过管道将第一个程序的 stdout 发送到另一个程序的输出到另一个程序的 stdin 第二个程序。这样,您可以确保输出和输入是可预测的。在这些情况下,scanf 实际上运作良好。但是,在处理不可预测的输入时,您会面临各种麻烦。

      那么为什么没有易于使用的标准函数供用户输入呢?在这里只能猜测,但我假设老铁杆 C 黑客只是认为现有功能已经足够好,即使它们非常笨重。此外,当您查看典型的终端应用程序时,它们很少从stdin 读取用户输入。大多数情况下,您将所有用户输入作为命令行参数传递。当然,也有例外,但对于大多数应用程序来说,用户输入是一件非常小的事情。

      那么你能做什么呢?

      首先,gets 不是替代品。这是危险的,永远不应该使用。在这里阅读原因:Why is the gets function so dangerous that it should not be used?

      我最喜欢的是fgetssscanf 的组合。我曾经写过一个答案,但我会重新发布完整的代码。这是一个不错的(但不是完美的)错误检查和解析的例子。它足以用于调试目的。

      注意

      我不太喜欢要求用户在一行中输入两个不同的内容。只有当它们以自然的方式属于彼此时,我才会这样做。比如printf("Enter the price in the format <dollars>.<cent>: "); fgets(buffer, bsize, stdin);,然后使用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, "%f%f", &f, &g)) != 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

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

      为避免行长于缓冲区的问题,您可以使用自动分配适当大小的缓冲区的函数,您可以使用getline()。缺点是之后您需要free 结果。标准不保证此功能存在,但 POSIX 有。您也可以实现自己的,或在 SO 上找到一个。 How can I read an input string of unknown length?

      加强游戏

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

      有趣的阅读

      这是关于scanf的咆哮:https://web.archive.org/web/20201112034702/http://sekrit.de/webdocs/c/beginners-guide-away-from-scanf.html

      【讨论】:

      • 请注意,(r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2 不会检测到结尾的非数字文本。
      • @chux 已修复 %f%f。第一个是什么意思?
      • scanf 旨在用于格式完美的数据 但即使这样也不是真的。除了@chux 提到的“垃圾”问题之外,还有一个事实是,像"%d %d %d" 这样的格式很乐意从一、二或三行(甚至更多,如果中间有空行)读取输入,没有办法通过执行"%d\n%d %d" 等操作来强制(比如说)两行输入。scanf 可能适用于格式化的 stream 输入,但它对任何事情都没有好处基于行。
      • @JosephQuinsey 谢谢
      • @chqrlie 不错。更新
      【解决方案3】:

      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 修改其输入(它用字符串终止符覆盖分隔符),并且您无法保留其状态(即,您不能部分标记一个字符串,然后开始标记另一个字符串,然后从原始字符串中的中断处继续)。有一个变体,strtok_s,它保留了标记器的状态,但 AFAIK 它的实现是可选的(您需要检查 __STDC_LIB_EXT1__ 是否已定义以查看它是否可用)。

      将输入标记化后,如果您需要将字符串转换为数字(即"1234" => 1234),您可以选择。 strtolstrtod 会将整数和实数的字符串表示形式转换为它们各自的类型。它们还允许您捕获我上面提到的12w4 问题 - 它们的一个参数是指向字符串中转换的第一个字符 not 的指针:

      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 以避免UB 在 char 签名的平台上。
      【解决方案4】:

      我可以用什么来解析输入而不是 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.
        }
      

      【讨论】:

        【解决方案5】:

        在这个答案中,我假设您正在阅读并且 解释文本行。 也许您正在提示正在输入内容的用户并且 击中返回。或者也许你正在阅读结构化的行 来自某种数据文件的文本。

        由于您正在阅读文本行,因此组织起来很有意义 您的代码围绕一个库函数读取,嗯,一行 文本。 标准函数是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);
        

        如果您希望用户键入单个字符(可能是 yn 作为是/否的回应),你可以直接抓住第一个 行的字符,如下所示:

        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"

        有时额外的换行符并不重要(比如我们调用 atoiatof,因为它们都忽略了任何额外的非数字 在数字后输入),但有时它很重要。所以 通常我们会想去掉那个换行符。有几种 方法来做到这一点,我会在一分钟内得到。 (我知道我已经 说了很多。但我保证会回到所有这些事情上。)

        此时,你可能会想:“我以为你说的是​​scanf 不好,这种其他方式会好得多。 但是fgets 开始看起来很讨厌。 致电scanf 非常如此简单!不能继续用吗?”

        当然,如果你愿意,你可以继续使用scanf。 (对于真的 简单的事情,在某些方面它更简单。)但是,请不要 当它因为它的 17 个怪癖之一而让你失望时来向我哭泣 和弱点,或者因为输入你的 没想到,或者当你想不通怎么用它来做 更复杂的东西。让我们看看fgets的 实际麻烦:

        1. 您始终必须指定数组大小。嗯,当然, 这根本不是一件麻烦事——这是一个功能,因为缓冲区 溢出是一件非常糟糕的事情。

        2. 您必须检查返回值。其实就是洗头 因为要正确使用scanf,你必须检查它的返回 也很有价值。

        3. 你必须把\n 去掉。这是,我承认,一个真实的 滋扰。我希望有一个我可以指出的标准函数 你到那个没有这个小问题。 (请没人 提出gets。)但与scanf's相比有17个不同 烦人,我会在任何一天接受fgets这个烦人的事情。

        那么如何你去掉那个换行符?方法有很多:

        (a) 明显方式:

        char *p = strchr(line, '\n');
        if(p != NULL) *p = '\0';
        

        (b) 复杂而紧凑的方式:

        strtok(line, "\n");
        

        不幸的是,doesn't work quite right 在空行中。

        (c) 另一种紧凑且略显晦涩的方式:

        line[strcspn(line, "\n")] = '\0';
        

        还有其他方法。我,我总是只使用 (a),因为它简单明了,如果不够简洁的话。 请参阅this questionthis question,了解更多(更多)关于从fgets 中剥离\n 给您的信息。

        现在已经不碍事了,我们可以回到另一个 我跳过的东西:atoi()atof() 的缺陷。 问题是它们没有给你任何有用的东西 成功或失败的标志:他们悄悄地忽略 尾随非数字输入,如果有,它们会悄悄返回 0 根本没有数字输入。首选的替代品—— 还具有某些其他优势——strtolstrtodstrtol 还允许您使用 10 以外的基数,这意味着您可以 获得%o%xscanf 的效果(除其他外)。 但是展示如何正确使用这些功能本身就是一个故事, 并且会过多地分散已经转向的东西 变成一个相当支离破碎的叙述,所以我不会说 现在有更多关于他们的信息。

        其余的主要叙述涉及您可能正在尝试的输入 解析比单个数字更复杂的或 特点。如果您想读取包含两个 数字,或多个空格分隔的单词,或特定的 标点符号?这就是事情变得有趣的地方,并且 如果您尝试,事情可能会变得复杂 使用scanf 做事,还有更多 选项现在您已经使用fgets 清晰地阅读了一行文本, 尽管所有这些选项的完整故事可能会填满 一本书,所以我们只能在这里触及表面。

        1. 我最喜欢的技巧是将队列分成 空格分隔的“单词”,然后对每个单词做进一步的处理 “单词”。这样做的一个主要标准功能是 strtok(这也有问题,也对整体进行了评分 单独讨论)。我自己的偏好是专用功能 用于构造指向每个断开的指针的数组 “word”,我描述的一个功能 these course notes。 无论如何,一旦你有了“词”,你就可以进一步处理 每一个,也许是相同的atoi/atof/strtol/strtod 我们已经看过的函数。

        2. 自相矛盾的是,尽管我们已经花费了相当多的 在这里花费时间和精力来弄清楚如何远离scanf, 另一种处理我们刚刚阅读的文本行的好方法 fgets 是将其传递给sscanf。这样,你最终得到 scanf 的大部分优点,但没有 缺点。

        3. 如果您的输入语法特别复杂,可能适合使用“正则表达式”库来解析它。

        4. 最后,您可以使用任何适合的 ad hoc 解析解决方案 你。您可以使用 char * 指针检查您期望的字符。或者你可以 使用 strchrstrrchr 等函数搜索特定字符, 或strspnstrcspnstrpbrk。或者您可以解析/转换 并使用strtol 跳过数字字符组或 strtod 我们之前跳过的函数。

        显然还有很多话要说,但希望 本介绍将帮助您入门。

        【讨论】:

        • 有充分的理由写sizeof (line) 而不是简单的sizeof line?前者使它看起来像 line 是一个类型名称!
        • @TobySpeight 有充分的理由吗?不,我怀疑。括号是我的习惯,因为我懒得记住是对象还是类型名称,但许多程序员在可能的时候将它们省略了。 (对我来说,这是个人喜好和风格的问题,而且是很小的问题。)
        • +1 用于使用sscanf 作为转换引擎,但使用不同的工具收集(并可能按摩)输入。但也许值得一提的是getline
        • 感谢您对sizeof 风格的解释。对我来说,记住何时需要括号很容易:我认为(type) 就像没有值的强制转换(因为我们只对类型感兴趣)。另一件事:您说strtok(line, "\n") 并不总是有效,但当它可能无效时并不明显。我猜您正在考虑行比缓冲区长的情况,所以我们没有换行符,strtok() 返回 null?很遗憾fgets() 没有返回更有用的值,因此我们可以知道换行符是否存在。
        • @TobySpeight: 如果行中只有一个 '\n'strtok(line, "\n"); 将不起作用。这是个大问题!此外strtok() 对全局隐藏变量有副作用...不要使用这种诡计和虚假的方式,完全远离strtok()
        【解决方案6】:

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

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

        • 无效输入必须被拒绝

        • 当任何输入被拒绝时,有必要向用户提供描述性消息,解释(以“非程序员的普通人易于理解”的语言)为什么它被拒绝(所以人们可以弄清楚如何解决问题)

        为了让事情变得非常简单,让我们考虑解析一个简单的十进制整数(由用户输入),仅此而已。用户输入被拒绝的可能原因有:

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

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

        • 前导空格和尾随空格将被忽略(例如“
          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;
        }
        

        满足规定的要求;这个convertStringToInteger() 函数本身很可能最终只有几百行代码。

        现在,这只是“解析一个简单的十进制整数”。想象一下,如果你想解析一些复杂的东西;比如“姓名、街道地址、电话号码、电子邮件地址”结构的列表;或者可能像一种编程语言。对于这些情况,您可能需要编写数千行代码来创建一个不是废话的解析。

        换句话说……

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

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

        【讨论】:

        • 这是唯一的方法。不要将 libc 用于绝大多数事情。
        【解决方案7】:

        下面是一个使用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);
        }
        

        【讨论】:

          【解决方案8】:

          其他答案给出了正确的低级细节,所以我将自己限制在更高级别:首先,分析您期望每个输入行的样子。尝试使用正式的语法来描述输入 - 如果幸运的话,您会发现它可以使用正则文法,或者至少是上下文无关文法来描述。如果常规语法就足够了,那么您可以编写一个有限状态机,它一次识别和解释每个命令行一个字符。然后,您的代码将读取一行(如其他回复中所述),然后通过状态机扫描缓冲区中的字符。在某些状态下,您停止并将到目前为止扫描的子字符串转换为数字或其他内容。如果这么简单,您可能可以“自己动手”;如果您发现需要完整的上下文无关语法,最好弄清楚如何使用现有的解析工具(回复:lexyacc 或其变体)。

          【讨论】:

          • 有限状态机可能是矫枉过正;检测转换溢出的更简单方法(例如在使用strtoll 后检查errno == EOVERFLOW)是可能的。
          • 为什么要编写自己的有限状态机,而 flex 让编写它们变得非常简单?
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-11-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-12-10
          相关资源
          最近更新 更多