【问题标题】:Unexpected repitition using fgets and sscanf使用 fgets 和 sscanf 的意外重复
【发布时间】:2016-03-26 02:37:28
【问题描述】:

这是我的代码的一部分。 getssscanf 的目的是扫描三个由一个空格分隔的变量。如果通过,则再次输出指令。否则,输出错误并退出程序。

我想使用 7 长度的 char 数组来限制行中的数量,只获得像 'g 3 3' 这样的格式。但我的代码似乎有问题。

#include <stdio.h> 

int main (void) {
    char line[7];
    char command;
    int x, y;

    while(1){
        /* problem: g  4 4 or g 4  4 can also pass */
        fgets(line, 7, stdin);
        nargs = sscanf(line, "\n%c %d %d", &command, &x, &y);

        if(nargs != 3){
          printf("error\n");
          return 0;
        }

        printf("%c %d %d\n", command, x, y);
    }
}

意外:

g  4 4
g 4 4
error

预期:

g 4 4
g 4 4
// I can continue type

谁能告诉我为什么它仍然会重复指令?

【问题讨论】:

  • 使用sscanf 时,格式字符串中的空格或换行符与输入中的零个或多个空白字符匹配。字符串g 4 4 适合 7 字节缓冲区,并且匹配格式,所以它通过了。
  • g 4 4 => "g 4 4\n" => fgets => "g 4 4\0" => 下一个 fgets "\n" => sscanf return 0;增加行缓冲区。
  • 是的,g 4 4 应该通过但g 4 4(g 和 4 之间的两个空格)应该失败。当我将限制更改为 6 个字符时,即使 g 4 4 也会失败。我想知道原因
  • 1) 想要输入整行,而不考虑任何额外的空格。所以增加输入缓冲区的长度很多,建议50字节。 2) 在调用 fgets() 时使用 sizeof( line) 所以只需要编辑一个地方。 3) 使用#define 语句来设置line[] 长度 4) 而不是sscanf(),遍历line[] 数组来检查格式,因为sscanf() 不会因为多余的空格而失败。

标签: c fgets scanf


【解决方案1】:

根据C11 standard, 7.21.6.2p5

由空白字符组成的指令通过读取输入直到第一个非空白字符(仍然未读取)或直到无法读取更多字符来执行。

这将\n 指令和两个空格字符描述为功能相同:它们将尽可能多地匹配输入中的连续空白(空格、制表符、换行符等)。

如果你想匹配一个空格(并且只有一个空格),我建议使用%*1[ ] 而不是空白指令。您可以使用 %*1[\n] 类似地丢弃换行符。例如,由于换行符出现在行尾

nargs = sscanf(line, "%c%*1[ ]%d%*1[ ]%d%*1[\n]", &command, &x, &y);

不幸的是,这并不能完全解决您的问题,就像the %d format specifier is also defined to discard white-space characters

跳过输入的空白​​字符(由isspace 函数指定),除非规范包含[cn 说明符

通过一些巧妙的技巧,您也许可以继续使用sscanf(或者更好的是,没有中间缓冲区的scanf),但是在比较了可维护性成本方面的替代方案之后,我们不妨只使用getchar,因此,如果您正在寻找解决问题的方法而不是您提出的问题的答案,我建议您使用 gsamaras answer

【讨论】:

【解决方案2】:

你在那里的东西不会起作用,因为如果用户输入一两个空格,sscanf() 不会被打扰。

您可以通过利用short circuiting 和使用getchar() 以一种简单的方式解决此问题,如下所示:

#include <stdio.h>
#include <ctype.h>

#define SIZE 100

int main(void) {
    int c, i = 0;
    char line[SIZE] = {0};
    while ((c = getchar()) != EOF) {
        // is the first char an actual character?
        if(i == 0 && !isalpha(c)) {
                printf("error\n");
                return -1;
        // do I have two whitespaces in 2nd and 4th position?
        } else if((i == 1 || i == 3) && c != ' ') {
                printf("error\n");
                return -1;
        // do I have digits in 3rd and 5th position?
        } else if((i == 2 || i == 4) && !isdigit(c)) {
                printf("error\n");
                return -1;
        // I expect that the user hits enter after inputing his command
        } else if(i == 5 && c != '\n') {
                printf("error\n");
                return -1;
        // everything went fine, I am done with the input, print it
        } else if(i == 5) {
                printf("%s\n", line);
        }
        line[i++] = c;
        if(i == 6)
                i = 0;
    }
    return 0;
}

输出:

gsamaras@gsamaras:~$ gcc -Wall px.c
gsamaras@gsamaras:~$ ./a.out 
g 4 4
g 4 4
g  4 4
error

【讨论】:

  • 太棒了!但我仍然想知道有没有办法在 sscanf 中修复它?因为我们只能使用 sscanf
  • @JenniferQ 好。我不知道,但我赞成你的问题,这可能会带来更多的人,祝你好运。
  • @JenniferQ 不幸的是,您无法阻止 %d 丢弃前缀空格。
  • 我不得不同意@Seb,顺便说一句,他给出了一个很好的答案。
  • 经过仔细分析,这是一个不错的答案。我花了一点时间来破译 OPs 代码的要求和此代码的要求之间的看似峡谷。不要误会我的意思,这不是你的错,但我认为你可以通过触摸它们轻轻地将你的答案推向这里最好的答案。举个例子:“只有 7 个字符的数组意味着每个十进制数字字段恰好是一个数字,但是 %d 通过允许多个数字(并且可能是负号)违反了这一点。我假设这些字段只应该每个数字都是一个数字。”
【解决方案3】:

谁能告诉我为什么它仍然会重复指令?

棘手的部分是"%d" 消耗前导空白,因此代码需要先检测前导空白。

" " 消耗 0 个或更多空白并且永远不会失败。

所以"\n%c %d %d" 不能很好地检测到中间空格的数量。


如果ints 可以超过 1 个字符,请使用这个,否则请参见下面的简化。

使用"%n检测sscanf()进度在缓冲区中的位置。

它使用sscanf() 完成工作,这显然是必需的。

// No need for a tiny buffer
char line[80];
if (fgets(line, sizeof line, stdin) == NULL) Handle_EOF();

int n[6];
n[5] = 0;
#define SPACE1 "%n%*1[ ] %n"
#define EOL1   "%n%*1[\n] %n"

// Return value not checked as following `if()` is sufficient to detect scan completion.
// See below comments for details
sscanf(line, "%c" SPACE1 "%d" SPACE1 "%d" EOL1, 
  &command, &n[0], &n[1],
  &x,       &n[2], &n[3],
  &y,       &n[4], &n[5]);

// If scan completed to the end with no extra
if (n[5] && line[n[5]] == '\0') {
  // Only 1 character between?
  if ((n[1] - n[0]) == 1 && (n[3] - n[2]) == 1 && (n[5] - n[4]) == 1) {
    Success(command, x, y);
  }
}

也许添加测试以确保command 不是空格,但我认为在命令处理中无论如何都会发生这种情况。


如果ints 只能是 1 位数字并且使用将 @Seb 答案与上述内容结合的 mod,则可以进行简化。这是可行的,因为每个字段的长度在可接受的答案中是固定的。

// Scan 1 and only 1 space
#define SPACE1 "%*1[ ]"

int n = 0;
// Return value not checked as following `if()` is sufficient to detect scan completion.
sscanf(line, "%c" SPACE1 "%d" SPACE1 "%d" "%n", &command, &x, &y, &n);

// Adjust this to accept a final \n or not as desired.
if ((n == 5 && (line[n] == '\n' || line[n] == '\0')) {
  Success(command, x, y);
}

@Seb 和我深入研究了检查sscanf() 的返回值的需要。尽管cnt == 3 测试是多余的,因为n == 5 仅在扫描整行并且sscanf() 返回3 时才为真,但许多代码检查器可能会引发一个标志,指出sscanf() 的结果未被检查。在使用保存的变量之前不限定sscanf() 的结果不是健壮的代码。这种方法使用n == 5 的简单而充分的检查。由于许多代码问题源于未进行任何限定,因此缺乏对sscanf() 的检查可能会在代码检查器中引发误报。添加冗余检查很容易。

// sscanf(line, "%c" SPACE1 "%d" SPACE1 "%d" "%n", &command, &x, &y, &n);
// if (n == 5 && (line[n] == '\n' || line[n] == '\0')) {
int cnt = sscanf(line, "%c" SPACE1 "%d" SPACE1 "%d" "%n", &command, &x, &y, &n);
if (cnt == 3 && n == 5 && (line[n] == '\n' || line[n] == '\0')) {

【讨论】:

  • 干得好 - 让我不必处理细节。
  • @Seb 这个答案没有有你描述的问题。注意n[5] = 0; sscanf(.....%n, ... &amp;n[5]); if (n[5] ...。对不起,如果这还不够简单。如果解析到达最后一个"%n"n[5] 将只有一个非零值。只有当sscanf() 的返回值为 3 时才会发生这种情况,在这种情况下。正如您所建议的那样,单独检查返回值是不够的,而不检查n[5],因为它不会检测最后一个"%d"之后的文本。
  • @Seb 按照您的建议将SPACE1 更改为"%n %n"不是%n%*1[ ] %n" 的改进。 OP想要检测“由一个空格分隔”。 "%n %n" 将有助于检测 1 个 white-space,因为您建议的格式中的 " " 接受任何空白字符,而 %*1[ ] " 将仅接受空格后跟空格。以下n[1] - n[0]) == 1 确保仅扫描了 1 个(空格)。
  • @Seb,您的测试用例不是这段代码,是一个问题,因为它在使用printf("x: %d... 之前没有检查sscanf() 的任何结果。就像您的代码 nargs = sscanf(line, "%c%*1[ ]%d%*1[ ]%d%*1[\n]", &amp;command, &amp;x, &amp;y); 后跟 printf() 一样也是一个问题。此代码不使用command, x, ,直到测试scanf() 的结果,if (n[5] 检查并且然后 使用Success(command, x, y);。我同意我认为 a) 保持开放的心态和 b) 学习如何(正确)使用 sscanf 并将其应用于所有是一个好主意。
  • %*1[ ] 的@Seb 问题与"%d" 相同。 IAC,这个答案不使用任何command, x, y,直到所有都被认为是有效的,即使是以合规方式完成的,有些人不熟悉。
【解决方案4】:

你的程序有问题吗? gdb 是你最好的朋友 =)

gcc -g yourProgram.c
gdb ./a.out
break fgets
run
finish
g 4  4

然后单步执行语句,无论何时遇到 scanf 或 printf 只需键入完成,您会看到程序成功完成了此迭代,但程序没有等待输入,只是打印错误消息?为什么 ?井型:

man fgets

fgets 最多读取 ONE LESS 小于 size,因此在您的情况下, fgets 只允许读取 6 个字符,但您给了它 7 个字符!是的,换行符就像空格一样是一个字符,那么第 7 个会发生什么?它将被缓冲,这意味着您的程序不会从键盘读取,而是会看到缓冲区中有字符并将使用它们(本例中为一个字符)。 编辑:这是使您的程序正常运行的方法
你可以忽略空行,如果( strccmp(line, "\n") == 0 )然后跳转到下一个迭代,如果你不允许使用 strcmp 一个解决方法是比较 line[0]=='\ n'。

【讨论】:

  • 如果stdin 被定义为无缓冲流怎么办?那它还会被缓冲吗?在那种情况下会发生什么?
  • stdin 在 linux 中是行缓冲的,如果你想解决这个问题,你将不得不使用终端而不是 stdio,至于文件,读取将是一个字符一个字符,你不能读取超过一个字符。
  • 嗯,我一定是错过了这个问题中的 linux 标签...
  • C 标准说 "What constitutes an interactive device is implementation-defined." 虽然对于你的 Linux 系统来说 stdin 是行缓冲的可能是有效的(它可能不是;稍后会详细介绍)为所有人保留。此外,如果您将文件以stdin 的形式通过管道输入,您会发现它可能不是 行缓冲,而是完全缓冲。有关这些条款的更多信息,以及如何(有时)更改(不接触终端)我建议阅读this
  • 是的,Unix 环境中的高级编程,第三版第 146 页。我知道缓冲区设置功能,但它们不适用于标准输入,如果您愿意,可以尝试.对于管道和文件,它们不是交互式设备,因此它们是完全缓冲的。 stackoverflow.com/questions/10247591/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-02-19
  • 2014-04-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-25
相关资源
最近更新 更多