【问题标题】:Splitting user input into strings of specific length将用户输入拆分为特定长度的字符串
【发布时间】:2017-03-29 22:32:37
【问题描述】:

我正在编写一个 C 程序,它将用户输入解析为一个字符和两个设定长度的字符串。用户输入使用fgets 存储到缓冲区中,然后使用sscanf 进行解析。问题是,这三个字段有一个最大长度。如果字符串超过此长度,则应消耗/丢弃下一个空格之前的剩余字符。

#include <stdio.h>
#define IN_BUF_SIZE 256

int main(void) {
    char inputStr[IN_BUF_SIZE];
    char command;
    char firstname[6];
    char surname[6];

    fgets(inputStr, IN_BUF_SIZE, stdin);
    sscanf(inputStr, "%c %5s %5s", &command, firstname, surname);
    printf("%c %s %s\n", command, firstname, surname);
}

所以,输入
a bbbbbbbb cc
所需的输出将是
a bbbbb cc
而是输出是
a bbbbb bbb

使用格式说明符"%c%*s %5s%*s %5s%*s" 会遇到相反的问题,即每个子字符串都需要超过设定的长度才能达到预期的结果。

有没有办法通过使用格式说明符来实现这一点,或者是在将子字符串削减到所需长度之前将子字符串保存在自己的缓冲区中的唯一方法?

【问题讨论】:

  • 我不认为scanf() 的字段描述符语言和整体语义足够强大,无法以传递给单个函数调用的单个格式字符串来表达您的要求。但是,您可以通过scanf() 调用的系列 来实现您想要的。
  • 请参阅How to use sscanf() in loops?,了解@JohnBollinger 提到的内容。您可能需要考虑将命令设置为 %1s 而不是 %c 以保持一致性(请记住也为空字节分配空间)。另请参阅How to prevent scanf() from causing buffer overflows?——尽管您显然知道答案的主要部分(您使用%ns,其中n 是一个数字)。
  • 如果您在没有sscanf() 的情况下做事,请考虑strcspn() — 和/或strpbrk() — 也可以考虑strspn();这些将允许您找到空白区域并决定字段的长度以及您将如何处理它。

标签: c string input split scanf


【解决方案1】:

除了其他答案之外,永远不要忘记在遇到字符串解析问题时,您始终可以选择简单地将指针向下移动到字符串以完成所需的任何类型解析。当您将字符串读入buffer(下面是我的buf)时,您有一个可以手动分析的字符数组(可以使用数组索引,例如buffer[i],或者通过分配一个指向开头的指针,例如@ 987654324@) 使用您的字符串,buffer 中有以下内容,p 指向buffer 中的第一个字符:

--------------------------------
|a| |b|b|b|b|b|b|b|b| |c|c|\n|0|    contents
--------------------------------
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4      index
 |
 p

要测试p 指向的字符,您只需取消引用 指针,例如*p。因此,要测试a-z 之间是否有一个初始字符,然后在buffer 的开头有一个空格,您只需要这样做:

    /* validate first char is 'a-z' and followed by ' ' */
    if (*p && 'a' <= *p && *p <= 'z' && *(p + 1) == ' ') {
        cmd = *p;
        p += 2;     /* advance pointer to next char following ' ' */
    }

注意:,您首先测试*p(这是*p != 0 或等效*p != '\0' 的简写)以验证字符串不为空(例如,第一个字符是't nul-byte) 在进行进一步测试之前。如果任何一项测试失败,您还可以包含 else { /* handle error */ }(这意味着您没有 command,后跟 space)。

当你完成后,你会留下p 指向buffer 中的第三个字符,例如:

--------------------------------
|a| |b|b|b|b|b|b|b|b| |c|c|\n|0|    contents
--------------------------------
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4      index
     |
     p

现在你的工作很简单,只前进不超过5 个字符(或者直到遇到下一个space,将字符分配给firstname,然后在最后一个字符之后分配nul-terminate

    /* read up to NLIM chars into fname */
    for (n = 0; n < NMLIM && *p && *p != ' ' && *p != '\n'; p++)
        fname[n++] = *p;
    fname[n] = 0;           /* nul terminate */

注意:因为fgets 读取并在buffer 中包含尾随'\n',您还应该测试换行符。

退出循环时,p 指向缓冲区中的第七个字符,如下所示:

--------------------------------
|a| |b|b|b|b|b|b|b|b| |c|c|\n|0|    contents
--------------------------------
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4      index
             |
             p

您现在只需向前阅读,直到遇到下一个 space,然后前进到 space,例如:

    /* discard remaining chars up to next ' ' */
    while (*p && *p != ' ') p++;

    p++;    /* advance to next char */

注意:如果您退出指向spacefirstname 循环,则上述代码不会执行。

最后,您所做的就是对 surname 重复与 firstname 相同的循环。将拼图的所有部分放在一起,您可以执行类似以下的操作:

#include <stdio.h>

enum { NMLIM = 5, BUFSIZE = 256 };

int main (void) {

    char buf[BUFSIZE] = "";

    while (fgets (buf, BUFSIZE, stdin)) {
        char *p = buf, cmd,                 /* start & end pointers */
            fname[NMLIM+1] = "",
            sname[NMLIM+1] = "";
        size_t n = 0;

        /* validate first char is 'a-z' and followed by ' ' */
        if (*p && 'a' <= *p && *p <= 'z' && *(p + 1) == ' ') {
            cmd = *p;
            p += 2;     /* advance pointer to next char following ' ' */
        }
        else {  /* handle error */
            fprintf (stderr, "error: no single command followed by space.\n");
            return 1;
        }

        /* read up to NLIM chars into fname */
        for (n = 0; n < NMLIM && *p && *p != ' ' && *p != '\n'; p++)
            fname[n++] = *p;
        fname[n] = 0;           /* nul terminate */

        /* discard remaining chars up to next ' ' */
        while (*p && *p != ' ') p++;

        p++;    /* advance to next char */

        /* read up to NLIM chars into sname */
        for (n = 0; n < NMLIM && *p && *p != ' ' && *p != '\n'; p++)
            sname[n++] = *p;
        sname[n] = 0;           /* nul terminate */

        printf ("input  : %soutput : %c %s %s\n",
                buf, cmd, fname, sname);
    }

    return 0;
}

使用/输出示例

$ echo  "a bbbbbbbb cc" | ./bin/walkptr
input  : a bbbbbbbb cc
output : a bbbbb cc

如果您有任何问题,请仔细查看并告诉我。无论字符串多么精细或您需要什么,您都可以通过简单地沿着字符串的长度移动一个指针(或一对指针)来获得所需的内容。

【讨论】:

  • 向新用户介绍walking a pointer 如果他们得到它,那是值得的。从那时起,您将永远不会面对任何无法解析的事情。然后问题变成为工作选择正确的工具,知道对于更长的字符串,标准库函数提供的效率确实可以提供帮助(循环展开等,它们为此目的而合并)。一个很好的例子是查看strlen 代码。
【解决方案2】:

根据 OP 的需要拆分输入缓冲区的一种方法是多次调用sscanf(),并使用%n 转换说明符来跟踪读取的字符数。在下面的代码中,输入字符串分三个阶段进行扫描。

首先,指针strPos被赋值为指向inputStr的第一个字符。然后使用" %c%n%*[^ ]%n" 扫描输入字符串。此格式字符串跳过用户可能在第一个字符之前输入的任何初始空格,并将第一个字符存储在command 中。 %n 指令告诉sscanf() 将到目前为止读取的字符数存储在变量n 中;然后*[^ ] 指令告诉sscanf() 读取并忽略任何字符,直到遇到空白字符。这有效地跳过了在初始 command 字符之后输入的任何剩余字符。 %n 指令再次出现,并用直到此时读取的字符数覆盖先前的值。使用%n 两次的原因是,如果用户输入一个字符后跟一个空格(如预期的那样),第二个指令将找不到匹配项,sscanf() 将退出而不会到达最终的%n 指令。

指针strPos 通过添加n 移动到剩余字符串的开头,并再次调用sscanf(),这次是"%5s%n%*[^ ]%n"。在这里,最多 5 个字符被读取到字符数组firstname[],读取的字符数由 %n 指令保存,任何剩余的非空白字符都被读取并忽略,最后,如果扫描使它成为至此,读取​​的字符数又被保存了。

strPos又增加了n,最后扫描只需要"%s"完成任务。

请注意,检查fgets() 的返回值以确保它成功。对fgets() 的调用略微更改为:

fgets(inputStr, sizeof inputStr, stdin)

这里使用sizeof 运算符代替IN_BUF_SIZE。这样以后如果inputStr的声明改了,这行代码还是正确的。请注意,sizeof 运算符在这里起作用,因为inputStr 是一个数组,并且数组不会衰减为sizeof 表达式中的指针。但是,如果 inputStr 被传递到函数中,sizeof 可以在函数内部以这种方式使用,因为在大多数表达式中,数组会衰减为指针,包括函数调用。有些人,@DavidC.Rankin,更喜欢 OP 使用的常量。如果这看起来令人困惑,我建议坚持使用常量 IN_BUF_SIZE

还要注意,每次调用sscanf() 的返回值都经过检查,以确保输入符合预期。例如,如果用户输入命令和名字,但没有姓氏,程序将打印错误消息并退出。值得指出的是,如果用户只输入命令字符和名字,则在第二个 sscanf() 之后匹配可能在 \n 上失败,然后 strPtr 递增以指向 @987654363 @ 等仍在界限内。但这依赖于字符串中的换行符。如果没有换行符,匹配可能会在\0 上失败,然后strPtr 将在下一次调用sscanf() 之前递增超出范围。幸运的是,fgets() 保留了换行符,除非输入行大于缓冲区的指定大小。然后没有\n,只有\0 终结符。一个更健壮的程序会检查输入字符串中的\n,并在需要时添加一个。增加IN_BUF_SIZE 的大小不会有什么坏处。

#include <stdio.h>
#include <stdlib.h>

#define IN_BUF_SIZE 256

int main(void)
{
    char inputStr[IN_BUF_SIZE];
    char command;
    char firstname[6];
    char surname[6];
    char *strPos = inputStr;        // next scan location
    int n = 0;                      // holds number of characters read

    if (fgets(inputStr, sizeof inputStr, stdin) == NULL) {
        fprintf(stderr, "Error in fgets()\n");
        exit(EXIT_FAILURE);
    }

    if (sscanf(strPos, " %c%n%*[^ ]%n", &command, &n, &n) < 1) {
        fprintf(stderr, "Input formatting error: command\n");
        exit(EXIT_FAILURE);
    }

    strPos += n;
    if (sscanf(strPos, "%5s%n%*[^ ]%n", firstname, &n, &n) < 1) {
        fprintf(stderr, "Input formatting error: firstname\n");
        exit(EXIT_FAILURE);
    }

    strPos += n;
    if (sscanf(strPos, "%5s", surname) < 1) {
        fprintf(stderr, "Input formatting error: surname\n");
        exit(EXIT_FAILURE);
    }

    printf("%c %s %s\n", command, firstname, surname);
}

示例交互:

a Zaphod Beeblebrox
a Zapho Beebl

fscanf() 函数以微妙且容易出错而闻名;上面使用的格式字符串可能看起来有点棘手。通过编写一个函数来跳到输入字符串中的下一个单词,可以简化对sscanf() 的调用。在下面的代码中,skipToNext() 将一个指向字符串的指针作为输入;如果字符串的第一个字符是\0 终止符,则指针原样返回。跳过所有初始的非空白字符,然后跳过任何空白字符,直到下一个非空白字符(可能是\0)。返回指向此非空白字符的指针。

生成的程序比前面的程序长一点,但可能更容易理解,而且它当然有更简单的格式字符串。该程序与第一个程序的不同之处在于它不再接受字符串中的前导空格。如果用户在command 字符之前输入空格,则认为这是错误输入。

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

#define IN_BUF_SIZE 256

char * skipToNext(char *);

int main(void)
{
    char inputStr[IN_BUF_SIZE];
    char command;
    char firstname[6];
    char surname[6];
    char *strPos = inputStr;        // next scan location

    if (fgets(inputStr, sizeof inputStr, stdin) == NULL) {
        fprintf(stderr, "Error in fgets()\n");
        exit(EXIT_FAILURE);
    }

    if (sscanf(strPos, "%c", &command) != 1 || isspace(command)) {
        fprintf(stderr, "Input formatting error: command\n");
        exit(EXIT_FAILURE);
    }

    strPos = skipToNext(strPos);
    if (sscanf(strPos, "%5s", firstname) != 1) {
        fprintf(stderr, "Input formatting error: firstname\n");
        exit(EXIT_FAILURE);
    }

    strPos = skipToNext(strPos);
    if (sscanf(strPos, "%5s", surname) != 1) {
        fprintf(stderr, "Input formatting error: surname\n");
        exit(EXIT_FAILURE);
    }

    printf("%c %s %s\n", command, firstname, surname);
}

char * skipToNext(char *c)
{
    int inWord = isspace(*c) ? 0 : 1;

    if (inWord && *c != '\0') {
        while (!isspace(*c)) {
            ++c;
        }
    }

    inWord = 0;

    while (isspace(*c)) {
        ++c;
    }

    return c;
}

【讨论】:

  • 请注意,fgets(inputStr, sizeof inputStr, ... 仅在声明了inputstr 的范围内有效,你有一个常量,更好的fgets(inputStr, IN_BUF_SIZE, ...。虽然语法上很好,sscanf(strPos, " %c%n%*[^ ]%n", &amp;command, &amp;n, &amp;n) - 只有一个n,但我不确定将其分配两次的验证值的意图。只是一个笨蛋,strPos -> strpos,把 camelCase 留给 C++,没什么问题,只是风格——所以这取决于你。
  • @DavidC.Rankin-- 同意camelCase,但 OP 使用它,所以我也这样做是为了保持一致性。我倾向于使用sizeof ... 而不是常量,以防止将来声明更改;但这是一个很好的说明,我将在我的回答中添加评论。也许我在回答中对此不够清楚,但是分配给 n 两次是因为如果第一个匹配是正确的大小(即一个字符后跟一个空格或一个5 个字符的字符串。在这种情况下,第一个赋值为n 提供了正确的字符数。如果扫描结束,n 将被覆盖。
  • 是的,我很确定情况就是这样,但我总是尝试为 OP 标记它,因为不管你信不信,这些小风格问题在你的代码给人的第一印象中响亮。至于n,是的,如果return1,我猜你有你的第一个n 要检查,但这会让你知道你的下一个字符是' 'EOF
  • @DavidC.Rankin-- 在第一个程序中,sscanf() 将返回 1,如果由于第二个被抑制,则进行一个或两个匹配,并且 %n 不会增加分配数数。如果第二次匹配失败,并且没有前面的%n,则没有分配给n,并且下一个扫描位置是未知的。如果第二次匹配成功并且没有后续%n,则错误的值存储在n中。因此,这两个任务都是必要的。