使用 fgets 和 sscanf 的意外重复

Unexpected repitition using fgets and sscanf

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

我想使用 7 长度的字符数组来限制行中的数量,只得到像 '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

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

根据 C11 standard, 7.21.6.2p5:

A directive composed of white-space character(s) is executed by reading input up to the first non-white-space character (which remains unread), or until no more characters can be read.

这说明 \n 指令和两个 space 字符在功能上是相同的:它们将匹配尽可能多的连续白色-space (spaces 、制表符、换行符等),因为它们可以来自输入。

如果你想匹配单个 space(并且只匹配单个 space),我建议使用 %*1[ ] 而不是 white-space 指令。您可以使用 %*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:

Input white-space characters (as specified by the isspace function) are skipped, unless the specification includes a [, c, or n specifier

通过一些聪明的 hack,您可以继续使用 sscanf(或者更好的是,scanf 没有中间缓冲区),但是在比较了可维护性成本方面的备选方案之后,我们不妨只使用 getchar,所以如果您正在寻找问题的解决方案而不是您提出的问题的答案,我建议使用

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

您可以通过利用 short circuiting and by using 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

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

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

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

man fgets

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

Can anyone tell me why it will still repeat the instruction?

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

" " 消耗 0 个或更多 white-space 并且永不失败。

所以"\n%c %d %d"没有很好地检测到中间spaces的数量。


如果 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]] == '[=10=]') {
  // 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 不是白色的space,但我认为这无论如何都会在命令处理中发生。


如果 int 必须只有 1 个数字,并且 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] == '[=11=]')) {
  Success(command, x, y);
}

@Seb 和我深入研究了检查 sscanf() 的 return 值的需要。尽管 cnt == 3 测试是多余的,因为 n == 5 仅在扫描整行时才为真,并且 sscanf() returns 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] == '[=12=]')) {
int cnt = sscanf(line, "%c" SPACE1 "%d" SPACE1 "%d" "%n", &command, &x, &y, &n);
if (cnt == 3 && n == 5 && (line[n] == '\n' || line[n] == '[=12=]')) {