程序内存布局:内存不是只有堆和栈 / Program Memory Layout
本文回答一个基础问题:内存和堆栈是什么关系?
简短说:内存是总概念,堆和栈只是进程地址空间里的两类常见区域。
1. 先把总图放出来
程序运行起来以后,会成为一个进程。
进程拥有自己的虚拟地址空间。可以先把它理解成:程序运行时能看到的一整套地址世界。
简化图如下:
+--------------------------------------------------+
| text |
| 函数编译后的机器指令 |
+--------------------------------------------------+
| rodata |
| 字符串字面量、只读常量 |
+--------------------------------------------------+
| data |
| 有初始值的全局变量、static 变量 |
+--------------------------------------------------+
| bss |
| 零初始化的全局变量、static 变量 |
+--------------------------------------------------+
| heap |
| 动态分配区域:new、malloc、容器扩容 |
+--------------------------------------------------+
| mmap / shared libraries |
| 动态库、内存映射文件、部分运行时区域 |
+--------------------------------------------------+
| stack |
| 函数调用帧、局部自动变量、返回地址 |
+--------------------------------------------------+真实系统会更复杂。
比如地址随机化、动态链接、线程栈、内存映射文件都会影响布局。
但学习 C++ 时,先抓住这件事:
同样叫“内存”,不同区域有不同用途和不同生命周期习惯。2. 用代码给对象找位置
看代码:
#include <string>
#include <vector>
int global_value = 10;
static int file_counter = 0;
int main() {
int local = 42;
static int local_static = 7;
const char* text = "hello";
std::string name = "Alice";
std::vector<int> values{1, 2, 3};
int* heap_int = new int(99);
delete heap_int;
}逐个看:
global_value
全局/静态区域
file_counter
全局/静态区域,只是名字只在当前文件可见
local
main() 这次调用的栈帧中
local_static
全局/静态区域,不随 main() 栈帧销毁
"hello"
通常在只读数据区域
text 这个指针变量
main() 栈帧中
name 这个 string 对象本体
main() 栈帧中
name 内部字符资源
可能在对象内部,也可能在动态存储区,取决于实现和字符串长度
values 这个 vector 对象本体
main() 栈帧中
values 的元素缓冲区
通常在动态存储区
heap_int 这个指针变量
main() 栈帧中
new int(99) 创建的 int 对象
动态存储区这里最重要的不是背答案,而是建立分层。
std::vector<int> values 这个对象本体可以在栈上。
但它内部管理的元素数组通常在堆上。
stack frame of main
+------------------------------+
| values object |
| begin ----+ |
| end ----|----+ |
| cap ----|----|----+ |
+------------|----|----|-------+
| | |
v v v
heap +------------------+
| 1 | 2 | 3 | ... |
+------------------+所以“这个变量在栈上还是堆上”有时问得太粗。
更准确的问题是:
对象本体在哪里?
对象内部是否拥有另一块资源?
那块资源在哪里?3. 代码区:函数也占内存
函数不是飘在空气里的。
函数体编译后会变成机器指令,放进程序的代码区域。
int add(int a, int b) {
return a + b;
}编译之后,大致是:
C++ source
|
v
compiler
|
v
machine instructions
|
v
text segment函数指针保存的是函数入口地址:
int (*fp)(int, int) = &add;fp 是一个变量。
它保存一个指向代码位置的地址。
但你不能 delete fp。
因为这段代码不是你用 new 申请的对象资源。
4. 只读数据区:字符串字面量在哪里
看代码:
const char* p = "hello";这里至少有两件东西:
p
一个指针变量
"hello"
一个字符串字面量大致关系:
stack frame
+------------------+
| p -------------- |----+
+------------------+ |
v
rodata +-------------+
| h e l l o 0 |
+-------------+字符串字面量通常放在只读区域。
所以不要写:
char* p = (char*)"hello";
p[0] = 'H';这不是“强转一下就能改”的问题。
你试图修改一段不应该修改的存储,结果可能崩溃,也可能表现怪异。
5. 全局对象和 static 对象
全局变量和静态变量有静态存储期。
静态存储期可以先理解成:
对象通常在程序启动附近创建,在程序结束附近销毁。例子:
int global_count = 0;
void tick() {
static int local_count = 0;
++local_count;
}local_count 的名字只在 tick() 里可见。
但它的对象不会在每次函数返回时销毁。
这里要分清:
作用域
名字在哪里能被写出来
存储期
对象的存储能维持多久
生命周期
对象什么时候被构造,什么时候被析构静态对象不是万能的。
它的问题包括:
初始化顺序可能复杂
全局状态让测试变难
多线程访问可能需要同步
析构顺序也可能影响程序结束阶段6. 栈和堆在总图中的位置
栈主要服务函数调用。
每次函数调用,通常会出现一个调用帧。
调用帧里可能有局部变量、返回地址、保存寄存器、临时空间。
堆主要服务动态分配。
运行时才知道大小、需要跨作用域存在、容器需要扩容,都会用到动态存储。
关系可以这样看:
function call stack
+------------------+
| frame of g |
+------------------+
| frame of f |
+------------------+
| frame of main |
+------------------+
|
| pointers / owners may refer to
v
heap
+------------------+
| dynamic objects |
| vector buffers |
| string buffers |
+------------------+栈和堆不是互相隔绝的。
栈上的对象可以拥有堆资源。
堆上的对象内部也可以有指针指向别处。
全局对象也可以拥有堆资源。
所以不要把内存问题简化成“这是不是堆”。
7. 本篇总结
内存和堆栈的关系是:
memory
|
+-- text:代码
+-- rodata:只读数据
+-- data/bss:全局和 static 数据
+-- heap:动态存储
+-- mmap:映射区域
+-- stack:函数调用工作区以后看到一个变量,按顺序问:
这个名字在哪里可见?
对象本体在哪里?
对象内部有没有另外拥有资源?
那块资源由谁释放?
对象什么时候构造和析构?下一篇讲栈与函数调用。理解栈以后,“为什么不能返回局部变量地址”就不再只是背规则。