数组存储在内存的哪里?

5

我正在尝试理解C程序中的内存管理。 我知道内存中有以下几个部分:

  1. 已初始化数据段
  2. BSS
  3. 代码

现在考虑以下程序:

#include <stdio.h>

int main(){
    int arr[4] = {1,2,3,4};
    int x = 10;

    printf("Hello World!");
}


在上面的程序中,arr和x都是在main函数内部局部声明的。我认为这意味着它们都将在函数堆栈上分配空间。然而,在Linux上运行size命令时,我发现数组实际上是在数据段中分配空间。
我在网上搜索过这个问题,但得到了矛盾的信息。有些答案说所有局部声明的变量都应该放在栈上,而另一些人则说数组应该放在堆上。我认为如果我在此示例中使用malloc动态分配内存,数组将进入堆中,但这不是情况。

2
请注意,您的初始化程序 {1,2,3,4} 将存储在程序映像的数据部分中。在运行时,如果您的程序甚至创建了数组变量(因为正如其他人所说,优化器可以删除没有影响的任何内容),它将在堆栈上为数组创建空间,然后将初始化程序复制到其中。或者,如果使用优化器,它可能会删除初始化程序并替换为内联寄存器设置指令。 - Zan Lynx
3个回答

5
我在网上搜索了这个问题,但是找到的信息互相矛盾。 请不要阅读随机博客之类的东西,它们通常有错误的信息。在Stack Overflow上,错误的信息往往会被downvote或者至少会有评论指出其不准确和谬误。
在上面的程序中,arr和x都在main函数内部局部声明。我认为这意味着它们都会在函数堆栈上分配空间。 C标准没有指定如何为对象分配内存。它只指定对象具有存储期,定义了变量对象的生命周期。
静态对象将从程序开始一直存在到结束; 自动对象将在包含声明(或复合文字)的最内层块{...}的生存期内存在,直到块结束; 线程本地对象将拥有线程的寿命; 分配的对象将从malloc/calloc/realloc/aligned_alloc到相应的free/realloc。
注:原文中的“variables”被划去,我猜测是因为译者想用更加准确的“objects”代替。
除此之外,C标准规定对象在其生命周期内将会:
- 为其保留内存 - 拥有一个恒定的地址(可通过使用 & 运算符观察)
现在,除此之外,还有所谓的“as-if”规则,它表示编译器可以生成任何程序代码,只要程序的外部行为相同,外部行为包括输入、输出、访问易失性对象等。
您程序中的变量具有自动存储期,这意味着每次进入主函数时,您将拥有具有新生命周期的新对象,直到主函数结束。通常,这意味着它们将存储在堆栈上,因为它将以最小的开销处理分配和释放。但是您的程序与其外部行为相同。
#include <stdio.h>

int main(void) {
    printf("Hello World!");
}

这意味着编译器可以完全消除这两个变量,并且不为其保留任何空间。
现在,如果您打印这些变量的地址
#include <stdio.h>

int main(void) {
    int arr[4] = {1,2,3,4};
    int x = 10;

    printf("Hello World! %p, %p\n", (void *)arr, (void *)&x);
}

因为变量的地址被使用并用于输出,C语言无法将它们优化掉。它们现在是否在堆栈上?好吧,C标准没有说明。它们需要从main函数开始至少具有生命周期直到结束 - 但是C编译器可以决定在堆栈上使用它们,因为该程序的外部行为将保持不变。
#include <stdio.h>

static int arr[4] = {1,2,3,4};
static int x = 10;
    
int main(void) {
    printf("Hello World! %p, %p\n", (void *)arr, (void *)&x);
}

这将把这些变量放在静态数据段中;当然地址会不同,但是C语言并不保证特定对象在内存中的位置,只保证它们有地址。


1
然而,当我在linux上运行size命令时,发现数组实际上是在数据段中分配空间的。 我认为您可能误解了您所看到的内容。 C标准对此没有说明。它只说arr具有自动存储期限。但是,大多数(如果不是全部)系统将在堆栈上保存x和arr。 尝试这个代码:
#include<stdio.h>

int main(){
    int arr[4] = {1,2,3,4};
    int x = 10;
    static int i = 0;

    printf("Hello World! arr is here %p and x is here %p\n", (void*)arr, (void*)&x);
    ++i;
    if (i < 3) main();
    
    return 0;
}

可能的输出:

可能的输出:

Hello World! arr is here 0x7ffcdaba4170 and x is here 0x7ffcdaba416c
Hello World! arr is here 0x7ffcdaba4140 and x is here 0x7ffcdaba413c
Hello World! arr is here 0x7ffcdaba4110 and x is here 0x7ffcdaba410c

即使这不是一个确凿的证据,它也强烈表明该系统正在使用堆栈,并且堆栈向较低地址增长,并且arrx都存储在该堆栈上。
顺便说一下:以可移植的方式打印堆栈指针是不可能的,但这是一个不错的阅读材料:打印堆栈指针的值

我明白你的观点。让我困惑的是,当我使用size a.out命令查找内存分配时。如果我不包括数组,只有一个变量,例如int i,那么内存就会有bss 8和data 600。但是当我将arr包含在程序中时,数据段变为608。现在,我尝试添加更多的变量,例如int a、b、c,并给它们一些值(否则它们可能会出现在BSS中!),但数据段没有改变!它仍然是608,如果我删除数组,它又变成了600!所以这就是让我困惑的地方。 - JANVI SHARMA
1
@JANVISHARMA 你可能忘记了,你本地数组的初始化值(即{1,2,3,4})也可能存储在程序中。顺便说一下,我刚刚在 Compiler Explorer 上调试了你的示例。在那里,您可以看到本地数组的空间被考虑在 sub rsp, 40 中(将堆栈指针向下移动以分配本地变量的空间),但初始化只是使用 mov 进行的 - 没有常量数据(而我的 printf() 的常量字符串是有常量数据的)。 - Scheff's Cat
@JANVISHARMA 如果你想尝试我的链接示例,可以尝试将编译器选项从“-O2”更改为“-O0”...;-) - Scheff's Cat

0
C语言中程序的存储方式如下:
全局变量——>数据段
静态变量——>数据段
常量数据类型——>代码和/或数据。考虑到一个常量本身将被存储在数据段中,并且对它的引用将被嵌入到代码中,可以考虑字符串字面值。
在函数中声明和定义的局部变量——>栈
在主函数中声明和定义的变量——>堆和栈
指针(例如:char *arr, int *arr)——>堆数据或栈,具体取决于上下文。C允许您声明全局或静态指针,在这种情况下,指针本身将最终位于数据段中。
动态分配空间(使用malloc、calloc、realloc)——>堆和栈
值得一提的是,“栈”在官方上被称为“自动存储类”。

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