堆内存简介
了解堆内存区域及其使用方式。
简介
回顾一下,代码编译过程的结果是一个可执行文件。该文件包含多个部分,例如:
-
静态段
- 它们在可执行文件中实际存在。运行时,这些段会被加载到内存中。
- 例子:
.text、.data、.rodata和.bss
-
动态段
- 它们在可执行文件中并不实际存在。可执行文件仅保存对内存区域的引用。
- 操作系统负责分配这些内存区域,并修改可执行文件中的引用,使其指向已分配的内存。
- 例子:
.stack和.heap
栈和堆共享同一块内存区域,但它们从不同的端点开始生长。栈从顶部开始向下增长,而堆从底部开始向上增长。
在执行 push 操作时,栈向下增长,为变量腾出空间;同样地,当我们请求内存时,堆也会增长(稍后会详细介绍)。
栈和堆都有最大大小限制,以确保它们不会无限增长并相互重叠。
以下是内存布局示意图:
动机
当需要分配较大的内存块时,堆非常有用。栈的空间较小,当我们尝试定义几个大数组时,很容易耗尽栈空间。
例如,下面这段代码在第 7 行因使用过多栈内存而导致段错误(segmentation fault)。
我们试图分配 9999999 * sizeof(long long) = 799999920 字节,约等于 0.8 吉字节 的数据。栈并非为大规模内存分配设计,无法容纳如此大的数组。
1#include <stdio.h> 2 3#define ARR_SIZE 9999999 4 5void func() 6{ 7 long long arr[ARR_SIZE]; 8 9 for(int i = 0; i < ARR_SIZE; i++) 10 { 11 arr[i] = i; 12 printf("%d\n", arr[i]); 13 } 14} 15 16int main() 17{ 18 func(); 19 20 return 0; 21}
让我们看看栈空间有多大。为此,我们可以调用 Linux 提供的 getrlimit 函数。
1#include <stdio.h> 2#include <sys/resource.h> 3 4int main(void) 5{ 6 struct rlimit stackLimit; 7 getrlimit(RLIMIT_STACK, &stackLimit); 8 printf("栈空间限制: %lu\n", stackLimit.rlim_cur); 9}
结果显示限制为 8388608 字节。理论上,这是我们可以一次性分配的最大内存。然而,栈还需要保存调用函数的栈帧、调用者的调用者栈帧等(如果存在的话)。
因此,我们无法分配一个总大小为 8388608 字节的数组,因为栈还需要存储其他数据,包括:
- 返回地址
- 参数
- 调用者的局部变量
实用性
有人可能会说,空间不是问题,因为我们通常不需要这么多数据。但实际上,内存需求可能迅速增长。
例如,考虑一个三维数组 long long arr[500][500][500],这看起来是一个相当合理的数组,对吧?
但它实际上占用了 500 * 500 * 500 * sizeof(long long) = 1000000000 字节 = 1 吉字节。
显然,栈无法容纳这么大的数据。虽然某些系统的栈空间更大,有些更小,但将如此大量的内存放在栈上是不可行的。
此外,如果我们在很深的调用栈中,甚至可能仅仅为了保存返回地址就耗尽栈空间。这在递归函数中是一个常见问题。
我们可以把大数组放在全局作用域中,但这会导致它们在整个程序中都可见,可能不符合我们的设计意图。
解决方案是将数据分配在堆上。我们说,堆允许我们进行动态内存分配。
注意:栈和堆之间还有更多区别,但目前只需记住,堆是另一个可以分配内存的区域。