【问题标题】:Why does scanf accept more characters than there is room for in the buffer?为什么 scanf 接受的字符多于缓冲区的空间?
【发布时间】:2026-02-15 16:10:01
【问题描述】:

见以下代码:

int main()
{
  char test[3];

  scanf("%s", test);
  __fpurge(stdin);
  printf("%s", test);
}

程序应该只记录 3 个字符,但是当我键入时,例如,8 个字符,程序会记录全部 8 个字符!这不应该发生。正确的会记录 3 个字符,因为 scanf 是这样做的吗?

【问题讨论】:

  • 这是未定义的行为。这意味着任何事情都可能发生,包括显然工作正常。我没有系鞋带。为什么我没有摔倒?这不是问题。
  • 您没有告诉scanf() 有多少空间可用于数据,因此它假定必须有足够的空间用于输入的任何内容。因此,它尽最大努力存储 8 个字符——但这会溢出您指定的数组,导致未定义的行为。如果您学习了一种具有缓冲区溢出自动保护功能的现代语言,那么学习 C 语言将会很困难。 C 环境不能保护您免受误解或粗心的行为。 (而char test[3] 只能容纳两个字符的字符串,加上空终止符。)
  • 很好奇,为什么你期望 C不接受超过缓冲区空间的字符?

标签: c buffer scanf


【解决方案1】:

scanf 接受的数据超出了test 所能容纳的数量,因为您可以通过无限制地使用%s 来允许它这样做。这是危险的,必须在生产代码中避免。

%s 替换为%3s 以解决此问题。如果要读取三个字符,test 必须是四个字符宽以容纳空终止符:

char test[4];
scanf("%3s", test);

【讨论】:

    【解决方案2】:

    当您将test 传递给scanf() 时,您传递的只是一个指向缓冲区第一个字符的指针,因此scanf() 不知道您的缓冲区有多大。它会很乐意接受您输入的任意数量的字符,并将它们全部存储在那里。因此,当您键入超过 2 个字符时,您将导致 scanf() 将字符(加上零 asciiz 终止符)写入缓冲区末尾。通常,在这种情况下会发生程序崩溃。

    您没有遇到崩溃的事实在很大程度上是巧合,可能发生的情况是编译器出于对齐考虑在堆栈中分配了超过 3 个字符的空间,可能是 8 个字符或更多的空间。如果你输入足够多的字符,你的程序肯定会崩溃。

    因此,scanf() 的这种用法被认为是完全不安全的。在进行任何严肃的编码时,绝不应该像那样使用scanf()。相反,您应该指定字符串的宽度,如下所示:"%2s"。 (请注意,您必须指定一个比缓冲区大小小 1 的数字,以说明将由 scanf() 自动附加的零 asciiz 终止符。)

    【讨论】:

    • @JonathanLeffler 是的,我刚刚做了,我正在查看文档。我记得一种格式字符串语法,您可以将缓冲区和缓冲区的大小作为参数传递,而不必将缓冲区的大小嵌入字符串文字中,但我找不到它。这可能是一些 Microsoft C 扩展。
    • @MikeNakis 将是 scanf_s,它需要额外的参数来确定某些类型的格式说明符的大小。它应该是“更安全”的,但实际上它和原来的 scanf 一样棘手,如果你弄错了,实际上是 more 危险的,因为您可能会错误地为 address 参数传递大小参数,例如 2
    • 你可能会想到printf(),你确实可以写%*.*s来指定最小字段宽度和“精度”(要显示的最大字符数),它将占用与参数列表中的* 对应的值。 scanf() 的问题之一是它没有等效的功能(在 scanf() 格式字符串中,%*s 表示读取但不分配字符串)。
    • @JonathanLeffler 是的,我记得很失望地发现scanf 使用* 的方式与printf 不同。在后者中,* 宽度是一个函数参数。不幸的是,AFAIK scanf 需要对其进行硬编码,除了 MSVC 扩展 scanf_s 需要 %s 的宽度参数