如何使用scanf允许输入空格?

179
```python print("Hello, World!") ```

使用下面的代码:


```python print("Hello, World!") ```
char *name = malloc(sizeof(char) + 256); 

printf("What is your name? ");
scanf("%s", name);

printf("Hello %s. Nice to meet you.\n", name);

用户可以输入他们的名字,但当他们输入一个带空格的名字,比如Lucas Aardvarkscanf()只会截取Lucas后面的所有内容。我该如何让scanf()允许空格?


11
注意,更符合惯用语的写法是'malloc(sizeof(char) * 256 + 1)',或者'malloc(256 + 1)',甚至更好的方式(假设'name'只在局部使用)是'char name[256+1]'。'+1'可以作为空终止符的记忆方法,需要包含在分配内存中。 - Barry Kelly
@Barry - 我怀疑 sizeof(char) + 256 是一个笔误。 - Chris Lutz
11个回答

246

对于人们(尤其是初学者),除非您确定输入始终具有特定格式(甚至可能仍然不行),否则不应使用scanf("%s")gets()或任何其他没有缓冲区溢出保护的函数。

请记住,scanf代表“扫描格式”,而用户输入数据很少是格式化的。如果您可以完全控制输入数据格式,则最理想,但通常不适合用户输入。

使用fgets()(具有缓冲区溢出保护)将输入获取到字符串中,并使用sscanf()进行评估。由于您只需要用户输入而不需要解析,因此在这种情况下,实际上并不需要sscanf()

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

/* Maximum name size + 1. */

#define MAX_NAME_SZ 256

int main(int argC, char *argV[]) {
    /* Allocate memory and check if okay. */

    char *name = malloc(MAX_NAME_SZ);
    if (name == NULL) {
        printf("No memory\n");
        return 1;
    }

    /* Ask user for name. */

    printf("What is your name? ");

    /* Get the name, with size limit. */

    fgets(name, MAX_NAME_SZ, stdin);

    /* Remove trailing newline, if there. */

    if ((strlen(name) > 0) && (name[strlen (name) - 1] == '\n'))
        name[strlen (name) - 1] = '\0';

    /* Say hello. */

    printf("Hello %s. Nice to meet you.\n", name);

    /* Free memory and exit. */

    free (name);
    return 0;
}

1
我之前不知道 fgets()。它看起来比 scanf() 更容易使用。+1 - Kredns
8
如果你只想从用户那里获取一行文本,那会更容易。这样也更安全,因为你可以避免缓冲区溢出的问题。scanf系列函数非常有用,可以将一个字符串转换成不同的内容(例如使用"%c%c%c%c%d"将其转换为四个字符和一个整数),但即使如此,你也应该使用fgets和sscanf而不是scanf,以避免可能发生的缓冲区溢出问题。 - paxdiablo
5
在scanf格式中,您可以设置最大缓冲区大小,但如果要在运行时计算缓冲区大小,则必须在运行时构建格式(与printf的相当,是scanf的有效修改器,具有另一种行为:禁止赋值)。 - AProgrammer
1
@JonathanKomar和未来阅读此内容的任何人:如果你的教授告诉你在作业中必须使用scanf,那么他们是错误的,你可以告诉他们我这么说,并且如果他们想与我争论,我的电子邮件地址可以轻松地从我的个人资料中找到。 - zwol
1
@Rainning:它的目的不是为了美观,否则我们就不会有strcscnpatoi。这是一个功能性的名称,可能意味着从文件句柄中获取。它的能力并不受其名称的影响,可以完成OP想要的功能。 - paxdiablo
显示剩余7条评论

149

请尝试

char str[11];
scanf("%10[0-9a-zA-Z ]", str);

14
(1) 显然,如果要接受空格,则需要在字符类中放置一个空格。 (2) 注意,10是将被读取的最大字符数,因此str必须指向至少为11的缓冲区。 (3) 这里的最后一个s不是格式指令,但scanf将尝试将其完全匹配。这将在输入1234567890s之类的条目时可见,其中s将被消耗但不会被放置在任何地方。其他字母将不会被消耗。如果在s之后放置另一个格式,则只有在有s要匹配时才会读取它。 - AProgrammer
1
另一个可能存在的问题是,使用“-”符号不在第一或最后一个位置时,其实现定义是不确定的。通常,它用于表示范围,但范围所代表的内容取决于字符集。EBCDIC 字符集中字母范围存在空缺,即使假定为 ASCII 衍生字符集,认为所有小写字母都在 a-z 范围内也是天真的想法... - AProgrammer
3
"%[^\n]"存在和gets()一样的问题,即缓冲区溢出。但是,它还有一个额外的陷阱,即最后的\n没有被读取;这会被事实掩盖,因为大多数格式都以跳过空格开始,但[不属于其中之一。我不理解在使用scanf读取字符串时的实例。 - AProgrammer
4
由于在早期的评论中指出,在某些情况下,输入字符串末尾的s是多余和不正确的,因此将其删除。[是自己的格式说明符,而不是s的某种变体。请注意保持原意,同时使翻译更加通俗易懂,但不要添加解释或其他内容。 - paxdiablo

69

这个例子使用了反转的扫描集,因此 scanf 会一直获取值,直到遇到换行符 '\n',因此空格也会被保存下来。

#include <stdio.h>

int main (int argc, char const *argv[])
{
    char name[20];

    // get up to buffer size - 1 characters (to account for NULL terminator)
    scanf("%19[^\n]", name);
    printf("%s\n", name);
    return 0;
}

1
小心缓冲区溢出。如果用户输入了50个字符的“名称”,程序可能会崩溃。 - brunoais
8
如您所知道缓冲区大小,可以使用 %20[^\n]s 来防止缓冲区溢出。 - osvein
1
45分,没有人指出明显的货物崇拜问题,即在那里使用s - Antti Haapala -- Слава Україні

30

您可以使用此功能

char name[20];
scanf("%19[^\n]", name);

或者这个
void getText(char *message, char *variable, int size){
    printf("\n %s: ", message);
    fgets(variable, sizeof(char) * size, stdin);
    sscanf(variable, "%[^\n]", variable);
}

char name[20];
getText("Your name", name, 20);

演示


3
我没有测试过,但根据本页面其他回答,我相信在你的例子中,scanf的正确缓冲区大小应为: scanf("%19[^\n]", name);(仍然因简洁的答案而加1)。 - DrBeco
1
只是作为一个旁注,sizeof(char)根据定义始终为1,因此无需乘以它。 - paxdiablo
@paxdiablo 我认为这并不适用于所有体系结构/平台。 - Vitim.us
2
@Vitim.us:如果你指的是我的sizeof(char) == 1评论,那是由标准规定的。例如,参见C11 6.5.3.4 /4:“当sizeof应用于具有类型charunsigned charsigned char(或其限定版本)的操作数时,结果为1”。有些人会犯一个错误,认为sizeof返回类型/变量中字节数的数量,16位的char将会给出两个字节。但这并不是事实,因为标准没有将“字节”定义为八个位,而是将其定义为“一系列连续的位,其数量由实现定义”。 - paxdiablo
sscanf(variable, "%[^\n]", variable);看起来像是一种可疑的方法来去掉\n。由于int sscanf(const char *restrict buffer, const char *restrict format, ...);中的bufferrestruct,我不认为你应该这样做。 - Ted Lyngmo
sscanf(variable, "%[^\n]", variable); 看起来像是一种可疑的去除\n的方式。由于int sscanf( const char *restrict buffer, const char *restrict format, ... ); 中的bufferrestrict限定的,我认为你不应该这样做。 - undefined

10
不要使用scanf()读取字符串而没有指定字段宽度。你还应该检查错误的返回值:
#include <stdio.h>

#define NAME_MAX    80
#define NAME_MAX_S "80"

int main(void)
{
    static char name[NAME_MAX + 1]; // + 1 because of null
    if(scanf("%" NAME_MAX_S "[^\n]", name) != 1)
    {
        fputs("io error or premature end of line\n", stderr);
        return 1;
    }

    printf("Hello %s. Nice to meet you.\n", name);
}

或者使用fgets()函数:

#include <stdio.h>

#define NAME_MAX 80

int main(void)
{
    static char name[NAME_MAX + 2]; // + 2 because of newline and null
    if(!fgets(name, sizeof(name), stdin))
    {
        fputs("io error\n", stderr);
        return 1;
    }

    // don't print newline
    printf("Hello %.*s. Nice to meet you.\n", strlen(name) - 1, name);
}

7

getline()

现在已成为POSIX标准的一部分。

它还解决了您之前提到的缓冲区分配问题,但您需要负责free内存。


标准?在你引用的参考文献中:“getline()和getdelim()都是GNU扩展。” - AProgrammer
1
POSIX 2008 添加了 getline。因此,GNU 在大约 2.9 版本的 glibc 中更改了它们的头文件,这给许多项目带来了麻烦。这里没有明确的链接,但可以在这里查看:https://bugzilla.redhat.com/show_bug.cgi?id=493941。至于在线手册页面,我选择了谷歌找到的第一个。 - dmckee --- ex-moderator kitten

6
您可以使用fgets()函数读取字符串,或使用scanf("%[^\n]s",name);,这样字符串读取将在遇到换行符时终止。

1
请注意,这并不会防止缓冲区溢出。 - brunoais
4
“s”不应该在那里。 - Antti Haapala -- Слава Україні

5
如果有人仍在寻找,以下是对我有效的解决方案-读取任意长度的包括空格的字符串。感谢网上许多发帖者分享这个简单而优美的解决方案。如果它起作用了,功劳归于他们,但错误都是我的。
char *name;
scanf ("%m[^\n]s",&name);
printf ("%s\n",name);

2
值得注意的是,这是一个POSIX扩展,不在ISO标准中存在。为了完整起见,您可能还应该检查errno并清理分配的内存。 - paxdiablo
3
“s”不应该在扫描集合后面出现。 - Antti Haapala -- Слава Україні
冗余的s。请不要提供垃圾答案。 - VimNing

0

您可以使用scanf函数来实现这个目的,只需要一点小技巧。实际上,您应该允许用户输入直到用户按下回车键(\n)。这将考虑每个字符,包括空格。以下是示例:

int main()
{
  char string[100], c;
  int i;
  printf("Enter the string: ");
  scanf("%s", string);
  i = strlen(string);      // length of user input till first space
  do
  {
    scanf("%c", &c);
    string[i++] = c;       // reading characters after first space (including it)
  } while (c != '\n');     // until user hits Enter
  string[i - 1] = 0;       // string terminating
return 0;
}

这是如何工作的?当用户从标准输入中输入字符时,它们将存储在字符串变量中,直到第一个空格。之后,剩余的输入将保留在输入流中,并等待下一个scanf。接下来,我们有一个for循环,从输入流中逐个获取字符(直到\n),并将它们附加到字符串变量的末尾,从而形成与用户从键盘输入相同的完整字符串。

希望这能帮助到某些人!


存在缓冲区溢出的风险。 - paxdiablo

-1

虽然你真的不应该使用scanf()来处理这种情况,因为有更好的调用方法,比如gets()getline(),但它是可以实现的:

#include <stdio.h>

char* scan_line(char* buffer, int buffer_size);

char* scan_line(char* buffer, int buffer_size) {
   char* p = buffer;
   int count = 0;
   do {
       char c;
       scanf("%c", &c); // scan a single character
       // break on end of line, string terminating NUL, or end of file
       if (c == '\r' || c == '\n' || c == 0 || c == EOF) {
           *p = 0;
           break;
       }
       *p++ = c; // add the valid character into the buffer
   } while (count < buffer_size - 1);  // don't overrun the buffer
   // ensure the string is null terminated
   buffer[buffer_size - 1] = 0;
   return buffer;
}

#define MAX_SCAN_LENGTH 1024

int main()
{
   char s[MAX_SCAN_LENGTH];
   printf("Enter a string: ");
   scan_line(s, MAX_SCAN_LENGTH);
   printf("got: \"%s\"\n\n", s);
   return 0;
}

3
gets 函数被废弃并从标准库中删除(参考 https://dev59.com/Bcvts4cB2Jgan1znax6k),这是有*原因*的。相比之下,`scanf` 函数即使存在问题也有安全的解决方式,所以 gets 更加糟糕。 - paxdiablo
“gets”函数为什么如此危险,以至于不应该使用它?从C99开始,“gets”函数已被弃用,早在2016年之前就已经被完全从C11中删除。 - phuclv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接