尽管存在 0 长度缓冲区和编译器警告,scanf() 仍然有效。到底是怎么回事?

scanf() works despite 0-length buffer and compiler warning. What is going on?

我的编译器 (clang) 显示此消息:

11:17:warning: format specifies type 'char *' but the argument has
      type 'char (*)[0]' [-Wformat]
    scanf("%s", &name);
           ~~   ^~~~~
1 warning generated.

来自以下代码(问候语程序):

/*
 * Program: gretting2.c
 * Utility: Display a greeting with your name.
 * Author:  Adrián Garro.
 */

#include <stdio.h>

int main () {

    char name[0];

    printf("-------------------\n");
    printf("Write your name: \n");
    printf("-------------------\n");

    scanf("%s", &name);

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

实际发生了什么,我该如何解决?

只需更改此:

scanf("%s", &name);

至:

scanf("%39s", name);

还有这个:

char name[0];

至:

char name[40];

此外,您还必须以 '\0' 结尾:

name[39] = '[=14=]'; 

因为正确的做法是

scanf("%s", name);
        /* ^ no ampersand

什么是

char name[0];

您应该指定一个非零长度并将其用于 scanf 长度说明符

scanf("%(length - 1)s", name);
        /*  ^ sunstitite with the value */

您正在阅读"string",因此正确的阅读方式是:

scanf("%s", name);

为什么编译器会报错?当您在 scanf 中提供参数时,您提供了变量的内存位置。例如:

int x;
scanf("%d", &x);

&xint *,即指向整数的指针,因此 x 将得到正确的值。

当您读取一个字符串时,您实际上是在同时读取许多 char 个变量。要存储它们,您需要一个 char * 数组;好吧,name 本身就是 char *,所以不需要写 &name。后者是char **,即char.

的二维数组

顺便说一下,你还需要分配space来读取字符。因此,您必须编写 char name[20](或任何其他数字)。您还需要在 int main().

中提供 return 0;

为了理解这一点,您必须了解 scanf 在做什么。 scanf 在这种情况下是从标准输入中读取一个字符串,并将其放入您提供的缓冲区中。 它不会为您分配 space,或检测溢出。您需要为您的字符串分配足够的 space。就目前而言,您正在为您的字符串分配 zero space,所以一切都是溢出。这是一个重大错误。

如果您的程序的用户写了超过 40 个字符,那么您作为另一个用户 suggests.What 而不是 char[0],而是 char[40]?这会导致未定义的行为。本质上,它会写入您不想写入的内存。它可能会导致段错误,可能会导致关键内存被覆盖,或者它可能会工作。这是 scanf. 的弱点 研究 fgets. 你告诉它缓冲区的大小,输入将被截断以适应。

当然,这与你的警告无关。您收到警告是因为引用数组的名称与引用指向其第一个元素的指针相同,即 name <==> &(name[0])。将指针指向 this 就像将指针指向指针,即 &name <==> &&(name[0])。由于 scanf 正在寻找类型为 char* 的参数,并且它正在获取指向它的指针,因此类型检查器会抱怨。

根据您希望它的稳健程度,您需要重新考虑该方法。我想首先是你是否理解你在声明 char name[ 0 ] 时使用的类型。这是一个 'zero-sized' 字节大小的字符数组。这是一件令人困惑的事情,如果它的行为因编译器而异,我也不会感到惊讶...

编译器抱怨的实际警告是类型不匹配。如果你获取数组中第一个字符的地址,你可以去掉它(即在 scanf 调用中使用 &( name[ 0 ] ))。 name 的地址是它在堆栈上的位置 - 碰巧数组实现使用相同的位置来存储数组数据,并且 name 在单独使用时被编译器区别对待这样数组的地址与其第一个元素的地址相同...

使用 char name[ 0 ] 会使您容易导致内存损坏,因为没有地方可以读取字符串,而实现细节可能只是运气好并允许它工作。解决此问题的一种简单方法是将 0 替换为有意义的数字,您将其取为输入字符串的最大长度。说 32 这样你就有了 char name[ 32 ] ......但是这不能处理更长字符串的情况。

由于我们生活在一个拥有大量内存和大堆栈的世界中,您可能可以 char name[ 4096 ] 并使用 4KB 内存作为缓冲区,这对于实际使用来说绝对没问题。

现在......如果你想变得有点肛门并处理病态情况,比如用户在按回车键之前睡着几个小时靠在一些键上并添加一些巨大的 8000 个字符长的字符串,有几种方法可以也可以用 'dynamic memory allocation' 来处理这个问题,但这可能有点超出了这个答案的范围。


顺便说一句,根据我的理解,char foo[ 0 ] 是故意有效的 - 它可能起源于 hack 并且具有令人困惑的类型,但并不少见地依赖于创建可变大小结构的老技巧如上所述 in this page from the GCC online docs

您的代码显示 "undefined behavior." 这意味着任何事情都可能发生。随便。

您正在将零长度数组传递给 scanf()。此外,您没有在格式字符串中传递数组长度。这会导致缓冲区溢出漏洞(总是在零长度目标数组的情况下)。

你需要这样的东西:

char name[51];
scanf("%50s", name);

注意 %50s 现在指定目标数组的大小(少一个,为空终止符留出空间!),这避免了缓冲区溢出。您仍然需要检查 scanf() 的 return 值,以及输入 name 是否实际上太长(您不会想在不告诉用户的情况下截断用户的输入)。

如果您使用的是 Linux,请查看名为 valgrind 的工具。它是一个运行时内存错误检测器(除其他外),有时可以为您捕获这样的错误(以及不太明显的错误,这是重点)。对于很多C程序员来说是不可或缺的。

OP 发布的代码存在几个问题

以下修复了大部分问题

我在评论中指出了问题所在

int main() {

//char name[0];  // this did not allow any room for the name
char  name[100] = {'[=10=]'}; // declare a 100 byte buffer and init to all '[=10=]'

printf("-------------------\n");
printf("Write your name:, max 99 char \n"); // 99 allows room for nul termination byte
printf("-------------------\n");

//scanf("%s", &name);  // this has no limit on length of input string so can overrun buffer
if( 1 == scanf("%99s", name) )   // 1) always check returned value from I/O functions
                                 // 2) no '&' before 'name' because
                                 //    arrays degrade to pointer to array when variable
                                 //    name is used
                                 // 3) placed max size limit on format conversion string
                                 //    so input buffer 'name' cannot be overflowed
{ // then scanf failed
    perror( "scanf failed for name" );
    return(-1);  // indicate error
}

// implied else, scanf successful

printf("------------------------------------\n");
printf("Hello %s, nice to meet you\n",name);
printf("------------------------------------\n");

return(0); // indicate success

} // 结束函数:main

  1. 字符名称[0]; ---> 字符名称[100];
    /* 你需要分配一些内存来存储名字 */

2.scanf("%s", &name);----> scanf("%s", 名称); /* scanf 将 char* 作为参数,因此您只需要传递字符串名称。 */

我不认为 scanf("%(length - 1)s", name);是需要的。 因为 %s 用于读取字符串。这将在到达第一个空白字符时停止,或在指定的字段宽度(例如“%39s”)处停止,以先到者为准。

除了这些并不经常使用。当然,您可以随心所欲地使用它们!

/

*
 * Program: gretting2.c
 * Utility: Display a greeting with your name.
 * Author:  Adrián Garro.
 */

#include <stdio.h>

int main () {

    char name[100];

    printf("-------------------\n");
    printf("Write your name: \n");
    printf("-------------------\n");

    scanf("%s", name);

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