C语言中函数和变量的内存分配

3
根据C编译器的版本和编译器标志,可以在函数的任何位置初始化变量(据我所知)。
我习惯于将所有变量放在函数顶部,但是有关变量在函数中的任何其他位置定义时内存使用情况的讨论已经开始。
下面我写了两个简短的例子,想知道是否有人能够解释(或验证)内存如何分配。
示例1:变量y在可能的返回语句之后定义,由于这个原因有可能不会使用该变量,据我所知,这并不重要,如果变量放在函数顶部,代码(内存分配)将是相同的。这正确吗?
示例2:变量x在循环中初始化,这意味着该变量的作用域仅限于此循环,但是这个变量的内存使用情况如何?如果将其放置在函数顶部,或者只是在函数调用时在堆栈上初始化,是否会有所不同?
编辑:总结一个主要问题: 减少变量的范围或更改第一次使用的位置(因此在任何其他位置而不是顶部)是否对内存使用产生影响?
代码示例1:
static void Function(void){
 uint8_t x = 0;

 //code changing x
 if(x == 2)
 {
  return;
 }

 uint8_t y  = 0;    
 //more code changing y
}

代码示例 2

static void LoopFunction(void){
 uint8_t i = 0;

 for(i =0; i < 100; i ++)
 {
  uint8_t x = i;
  // do some calculations
  uartTxLine("%d", x); 
 }

 //more code
}
3个回答

6

我习惯在函数顶部放置所有变量。

在旧版的C语言中,这是必需的,但现代编译器已经取消了这个要求。只要编译器在第一次使用变量时知道变量的类型,它就拥有了所有必要的信息。

我想知道是否有人能够解释内存如何分配。

编译器决定在自动存储区域中如何分配内存。实现不局限于为你声明的每个变量分配单独的位置。它们可以重用超出作用域的变量以及某个时间点后不再使用的变量的位置。

在你的第一个示例中,变量y可以使用先前由变量x占用的空间,因为y的第一个使用点在x的最后一个使用点之后。

在你的第二个示例中,循环内使用的x的空间可以重用于你可能在// more code区域声明的其他变量中。


好的,这意味着编写代码时这种方法比将所有变量放在顶部更有效率,因为在顶部可能不清楚何时超出范围或何时使用。这是否仍然无关紧要? - koldewb
6
编译器并不关心这个,因为它可以推断出变量的活动使用范围,并相应地分配内存。但对于阅读你代码的人来说很重要,所以把变量放在靠近使用它们的地方比在顶部声明所有变量更好。 - Sergey Kalinichenko

1
基本上,故事是这样的。在原始汇编中调用函数时,通常习惯于在进入函数时将函数使用的所有内容存储在堆栈上,并在离开函数时清理它。某些CPU和ABI可能具有涉及参数自动堆叠的调用约定。
很可能是因为这个原因,C和许多其他旧语言要求所有变量必须在函数顶部(或作用域顶部)声明,以便{ }反映堆栈上的推送/弹出。
大约在80年代/90年代左右,编译器开始高效地优化这种代码,也就是说,他们只会在第一次使用局部变量时分配房间,并在不再需要时进行释放。无论变量在哪里声明-对于优化编译器来说都无关紧要。
同时,C++取消了C的变量声明限制,允许变量在任何地方声明。然而,在1999年更新的C99标准之前,C实际上并没有解决这个问题。在现代C中,您可以在任何地方声明变量。
所以,除非你使用的是极为古老的编译器,否则你的两个示例之间绝对没有性能差异。然而,将变量的作用域尽可能缩小被认为是良好的编程实践 - 虽然不应该以可读性为代价。虽然这只是一种风格问题,但我个人更喜欢像这样编写你的函数:(请注意,您正在使用错误的printf格式说明符uint8_t)
#include <inttypes.h>

static void LoopFunction (void)
{
  for(uint8_t i=0; i < 100; i++)
  {
    uint8_t x = i;
    // do some calculations
    uartTxLine("%" PRIu8, x); 
  }

 //more code
}

0

旧版的 C 语言只允许在块的顶部声明(和初始化)变量。你可以在块内的任何位置初始化一个新的块(一对 {} 字符),因此你有可能在使用它们的代码旁边声明变量:

... /* inside a block */
{ int x = 3;
    /* use x */
} /* x is not adressabel past this point */

switch 语句、if 语句以及 whiledo 语句中(任何可以初始化新块的地方),您都可以这样做。

现在,您可以在允许语句的任何地方声明变量,并且该变量的作用域从声明点到您将其声明为内部嵌套块的末尾。

编译器决定何时为局部变量分配存储空间,因此在创建堆栈帧时可以将它们全部分配(这是gcc的方式,因为它仅分配一次局部变量),或者在进入定义块时分配(例如,Microsoft C就是这样做的)。在运行时分配空间需要在运行时推进堆栈指针,因此如果每个堆栈帧仅执行一次,则可以节省CPU周期(但浪费内存位置)。重要的是,在其作用域定义之外引用变量位置是不允许的,因此如果尝试这样做,您将得到未定义的行为。我发现了一个长期运行在互联网上的旧错误,因为没有人花时间使用Microsoft-C编译器编译该程序(它会失败并导致核心转储),而是通常使用GCC进行编译。代码在某些其他部分通过引用内部作用域(if语句的then部分)中定义的局部变量,因为所有内容都在main函数中,所以堆栈帧一直存在。Microsoft-C只是在退出if语句时重新分配空间,但GCC则等到main结束才这样做。通过仅向变量声明添加static修饰符(使其全局),问题得到解决,无需进行更多重构。
int main()
{
    struct bla_bla *pointer_to_x;
    ...
    if (something) {
        struct bla_bla x;
        ...
        pointer_to_x = &x;
    }
    /* x does not exist (but it did in gcc) */
    do_something_to_bla_bla(pointer_to_x); /* wrong, x doesn't exist */
} /* main */

当改变为:

int main()
{
    struct bla_bla *pointer_to_x;
    ...
    if (something) {
        static struct bla_bla x;  /* now global ---even if scoped */
        ...
        pointer_to_x = &x;
    }
    /* x is not visible, but exists, so pointer_to_x continues to be valid */
    do_something_to_bla_bla(pointer_to_x); /* correct now */
} /* main */

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