为什么在不同作用域中声明的同名变量被赋予相同的内存地址?

5

我知道在while循环中声明一个char[]变量是有作用域的,我看到了这篇帖子:在C语言中重新声明变量

阅读一个关于在C中创建简单web服务器的教程时,我发现我必须手动清除赋予responseData的内存,否则就会把index.html的内容不断附加到响应中,并且响应包含来自index.html的重复内容,如下所示:

while (1)
{
  int clientSocket = accept(serverSocket, NULL, NULL);
  char httpResponse[8000] = "HTTP/1.1 200 OK\r\n\n";
  FILE *htmlData = fopen("index.html", "r");
  char line[100];
  char responseData[8000];
  while(fgets(line, 100, htmlData) != 0)
  {
      strcat(responseData, line);
  }
  strcat(httpResponse, responseData);
  send(clientSocket, httpResponse, sizeof(httpResponse), 0);
  close(clientSocket);
}

由以下进行更正:

while (1)
{
  ...
  char responseData[8000];
  memset(responseData, 0, strlen(responseData));
  ...
}

对于来自JavaScript的人来说,这很令人惊讶。为什么我要声明一个变量并访问同名不同作用域下的变量的内存内容呢?为什么C不会在幕后重置那个内存呢?

此外...为什么不同作用域中同名的变量被分配相同的内存地址呢?

根据这个问题:Variable declared interchangebly has the same pattern of memory address 并非如此。然而,我发现这种情况相当可靠。


4
这个变量的存储类型是“自动”。这种存储类型不会被初始化,因此在开始时它可以有任何值(包括上一次迭代中的值)。为什么?因为它是按照这种方式定义的,这样做可以给编译器(和编译后的代码)提供不必要的工作自由。 - Eugene Sh.
3
如果没有必要,强制将每个变量初始化为零将浪费CPU周期。 - Christian Gibbons
2
如果你总是将responseData作为字符串使用,那么你可以保持7999字节未初始化,并简单地将'\0'赋值给第一个:char responseData[8000]; *responseData = 0; - pmg
1
如果你想初始化它(就像使用initializer一样),你可以这样做:char responseData[8000] = {0};。在底层,它将被编译成与memset等效的内容。 - Eugene Sh.
2
使用 *responseData = 0; 只会将前面未清除的8000个字节中的第一个字节写入,这将使对象成为长度为零的正确字符串。后续的字符串操作(特别是 strcat() 函数)将会完成正确的任务。 - pmg
显示剩余7条评论
3个回答

8

不完全正确。您不需要清除整个responseData数组 - 只清除其第一个字节就足够了:

 responseData[0] = 0;

正如 Gabriel Pellegrino 在 评论 中指出的那样,更符合习惯用语的表达是

 responseData[0] = '\0';

它明确地通过其零值代码点定义字符,而前者使用一个int常数零。在两种情况下,右侧参数的类型为int,该类型隐式转换(截断)为char类型进行赋值。(由于pmg的评论comment已修复该段落。)
您可以从strcat文档中了解到:函数附加其第二个参数字符串到第一个字符串。如果您需要将第一个块存储到缓冲区中,则需要将其附加到空字符串上,因此您需要确保缓冲区中的字符串为空。也就是说,它仅包含终止NUL字符。memset整个数组是过度的,因此浪费时间。
此外,对数组使用strlen可能会引发问题。您无法知道为数组分配的内存块的实际内容。如果它尚未被使用或自上次使用以来已被覆盖为其他数据,则可能不包含NUL字符。然后strlen将超出数组范围,导致未定义行为。即使成功返回,它也会给出比数组大小更大的字符串长度。结果memset将超出数组范围,可能覆盖一些重要数据!在memset数组时,请始终使用sizeof
memset(responseData, 0, sizeof(responseData));

编辑

在上面,我试图解释如何修复您的代码问题,但我没有回答您的问题。这里是:

  1. 为什么不同作用域中的变量(...)被分配相同的内存地址?

就执行而言,while(1) { ... }循环的每次迭代确实创建一个新的作用域。然而,在新作用域创建之前,每个作用域都会终止,因此编译器在堆栈上保留适当的内存块,并且循环在每次迭代中重复使用它。这也简化了编译后的代码:每次迭代都由完全相同的代码执行,该代码只是跳转到开头。循环内访问局部变量的所有指令在每次迭代中使用完全相同的寻址(相对于堆栈)。因此,下一次迭代中的每个变量在内存中具有与所有先前迭代中完全相同的位置。

  1. 我发现我必须手动清除内存

是的,在C语言中,自动变量在堆栈上分配时,默认情况下未初始化。我们需要在使用之前明确地赋予一个初始值,否则该值是未定义的,可能是不正确的(例如,浮点变量可能出现非数字,字符数组可能没有终止符,enum变量可能具有超出枚举定义的值,指针变量可能不指向有效的可访问位置等)。

  1. 否则内容(...)只会不断追加

这个问题在上面已经回答过了。

  1. 从JavaScript转过来,这很令人惊讶

是的,JavaScript显然会在新作用域中创建新变量,因此每次你都会得到一个全新的空数组。在C中,你只会得到先前分配的内存区域的同一部分用于自动变量,并且初始化它是你的责任。

另外,请考虑两个连续的循环:

void test()
{
    int i;

    for (i=0; i<5; i++) {
        char buf1[10];
        sprintf(buf1, "%d", i);
    }

    for (i=0; i<1; i++) {
        char buf2[10];
        printf("%s\n", buf2);
    }
}

第一个循环将五个数字的单个数字字符表示打印到字符数组中,每次都覆盖它 - 因此 buf1[] 的最后一个值(作为字符串)是 "4"

你期望第二个循环输出什么?一般来说,我们不知道 buf2[] 将包含什么,并且对其进行 printf 可能会导致未定义行为。但是,我们可以假设两个不同范围内的相同变量(即单个 10 项字符数组)将以相同方式分配在堆栈的相同部分。如果是这种情况,我们将从(形式上未初始化的)数组中获得数字 4 的输出。

此结果取决于编译器的构造,并应视为巧合。不要依赖它,因为这是未定义行为!

  1. C 为什么不自动重置那些“幕后”的内存?
因为它没有被告知。该语言的创建是为了编译出有效、紧凑的代码。它尽可能地少做“幕后”的工作。其中一件它不会做的事情是不会初始化自动变量,除非被告知。这意味着您需要在本地变量声明中添加显式的初始化程序或在第一次使用之前添加初始化指令(例如分配)。(这不适用于全局、模块范围的变量;这些变量默认初始化为零。)
在高级语言中,一些或所有变量在创建时都会被初始化,但在C语言中不会。这是它的特点,我们必须接受它 - 或者只是不使用这种语言。

2
为了符合惯用法,请将responseData[0]更改为'\0'; - Gabriel Pellegrino
另外,如果您不将char数组作为字符串处理,答案是否仍然正确?我的意思是,如果这个char数组包含图像,当'\0'不表示其结尾时会怎样? - Gabriel Pellegrino
1
'\0' 是一个 int,绝对等于 0 - pmg
1
@M.M 是的,但这会不必要地初始化数组的所有8000个项目,而实际上只需要初始化第一个字符。 - CiaPan
@pmg 没错。已修复。谢谢! - CiaPan
显示剩余2条评论

6

使用这行代码:

char responseData[8000];

你在告诉编译器:“嘿,大C,给我一块8000字节的区域,命名为responseData。”
在运行时,如果你没有指定,就没有人会清理或提供一个“全新”的内存块。这意味着你在每个单独的执行中得到的包含了这8000字节所有可能比特排列的内存块。有时候会发生一些非凡的事情,即你在每个执行中获取相同的内存区域,因此,这8000字节中的比特位也是相同的,就好像是第一次由大C授权给你的内存区域。所以,如果你不清理,你会觉得你在使用相同的变量,但实际上并不是!你只是在使用相同(从未清理过)的内存区域。
我要补充的是,作为程序员,清理你分配的内存(无论是动态还是静态)是你的责任之一。

1
评论不适合进行长时间的讨论;此对话已被移至聊天室 - Samuel Liew

3
为什么我想声明一个变量并访问在不同作用域中声明的同名变量的内存内容?为什么 C 不会在幕后重置该内存?
自动存储期对象(即块作用域变量)不会自动初始化它们的初始内容是未确定的。请记住,C 是 1970 年代早期的产物,注重运行时速度而非方便性。C 的哲学是,程序员最了解是否应将某些内容初始化为已知值,并且如果需要,足够聪明以自己实现。
虽然在每个循环迭代中您会 逻辑上 创建和销毁 responseData 的新实例,但事实上每次都在重复使用相同的内存位置。我们认为,在进入块时为每个块作用域对象分配空间并在离开块时释放空间,但在实践中通常情况下并非如此 - 在函数进入时为所有 块作用域对象分配空间,在函数退出时释放空间。
不同作用域中的不同对象可能映射到幕后相同的内存。考虑以下内容:
void bletch( void )
{
  if ( some_condition )
  {
    int foo = some_function();
    printf( "%d\n", foo );
  } 
  else
  {
    int bar = some_other_function();
    printf( "%d\n", bar );
  }

无法同时存在foobar,因此没有理由为两者分别分配空间 - 编译器通常会在函数入口处为一个int对象分配空间,并根据所取的分支使用该空间中的foobar
因此,responseData发生什么情况是,在函数入口处分配了一个8000个字符的数组的空间,并且每次迭代都使用相同的空间。这就是为什么您需要在每次迭代中清除它,可以使用memset调用或初始化程序来完成清除工作。
char responseData[8000] = {0}; 


  1. 正如M.M在评论中指出的那样,对于可变长度数组(以及其他可变修改类型),空间是根据需要预留的,虽然从语言定义上没有明确指定从哪里取得这些空间。但对于所有其他类型,通常的做法是在函数进入时分配所有必要的空间。


函数内所有块级作用域对象的空间在函数进入时分配,并在函数退出时释放。可变长度数组直到执行到其定义位置才被分配。 - M.M

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