1507 字
8 分钟
内存中的栈与堆
p29(13 分钟)
这一节没有新算法,全是程序运行时的内存模拟,但它是理解递归和指针的关键铺垫。
程序内存的四个区段
| 区段 | 用途 | 大小 |
|---|---|---|
| 文本段 | 存储程序中所有指令(编译后的机器语言形式) | 编译时确定,固定 |
| 全局变量区 | 存储所有全局变量(所有函数之外声明的变量) | 编译时确定,固定 |
| 栈 | 函数调用执行的临时空间;所有局部变量都存在栈中 | 编译时确定,固定 |
| 堆 | 动态内存,可按需增长或收缩 | 运行时增长 |
核心区别就一条:
其他所有区段大小在编译时确定且固定堆在运行时可以增长
运行期间我们无法控制其他区段的内存分配或释放但可以控制堆中的分配和释放这是理解「为什么链表/树的节点必须建在堆上」的根本原因。
栈帧
每当调用一个函数,都会从栈中为其执行分配一定量的内存,这块内存称为该函数调用的栈帧。
所有局部变量和函数调用的执行状态都存储在该函数调用的栈帧里每当一个函数调用结束,为其分配的栈帧都会被回收栈中内存在函数调用结束时自动释放。
逐步模拟:插入 15、10、20、25
前提:root 是 main 的局部变量。
main 开始执行
为 main 分配栈帧局部变量 root(指向 BST 节点的指针)未初始化 → 设为 NULL(NULL 只是地址 0 的一个宏)调用 insert(root, 15)
main 的执行暂停为 insert 分配新栈帧,局部变量 root、data 收集本次调用的参数此调用中 root 为空 → 进入第一个 if → 调用 getNewNodeinsert 再次暂停为 getNewNode 分配栈帧,局部变量:data(收参数)和 node* newNodenew 在堆中创建节点,假设得到地址 200 → 存入 newNode → 设置三字段:data = 15、左子 = NULL、右子 = NULLgetNewNode 返回 200 并结束 → 其栈帧被回收insert 在这一行恢复:root(insert 的局部变量)= 200 → insert 返回该 root 里的地址 200 并结束 → 栈帧回收main 恢复,main 的 root 被设为 200调用 insert(root, 10)
main 暂停 → 新 insert 栈帧此调用 root = 200 不为空 → 比较 10 < 15 → 走递归分支此 insert 暂停 → 为另一次 insert 调用分配新栈帧 传入 root = 0(200 的左孩子为空)、data = 10新调用中 root == NULL → 进入第一个 if → 调用 getNewNode → 堆中创建节点,假设地址 150 → 返回 150 → 栈帧回收上层 insert 恢复:200 节点的左孩子 = 150(链接建立) → 该 insert 结束,返回 200main 恢复:root 被重写为 200(与之前相同,无变化)调用 insert(root, 20)
堆中有一个值为 20 的节点作为 200 节点的右孩子,假设地址 300→ 200 节点的右孩子地址被设为 300调用 insert(root, 25)
main 暂停 → insert 调用,传入 root = 200、data = 2525 > 15 → 进入最后的 else(右子树插入) → 发起递归调用:传入 root = 300、data = 25该调用中 25 > 20 → 又一次进入最后 else → 再递归:传入 root = 0(右子树为空)该调用 root == NULL → 进入第一个 if → getNewNode → 假设地址 100 → 返回 100顶层调用恢复:把新节点地址 100 设为右孩子 → 该调用完成,返回 300下层调用恢复:从最后 else 里那一行继续 → 300 节点的右孩子 = 100 → 返回 300再下层:从最后 else 继续 → 200 节点的右孩子被设置为 300(覆盖后不变)main 恢复:root 被设为本次 insert 调用的返回值,用相同的值 200 覆盖递归的本质
一个函数自己调用自己,跟一个函数 A 调用另一个函数 B 没什么不同。所以每次递归调用都会分配一个新的栈帧。
结论
关键在最后一步:main 里的 root,以及节点中的所有链接,都必须得到恰当的更新。
很多 bug 就出在这儿——代码里丢了某些链接,或者多建了不必要的链接。这就是「必须接住返回值」的深层原因:root->left = insert(root->left, data) 这个赋值动作,就是「把链接更新回去」的那一步。漏了它,链接就丢了。
堆 vs 栈 的生命周期对比
| 栈 | 堆 | |
|---|---|---|
| 分配时机 | 函数调用时自动 | 运行时主动请求(malloc / new) |
| 释放时机 | 函数调用结束时自动释放 | 必须显式 free / delete |
| 可控性 | 无法控制 | 可以控制 |
| 不释放会怎样 | 自动回收 | 该内存一直处于已分配状态直到程序结束 |
堆中申请的任何内存都必须用 C 的 free 或 C++ 的 delete 显式解除分配,否则该内存一直处于已分配状态直到程序结束。这就是内存泄漏的定义。
收尾
内存本身是一种线性结构。
树能适配进线性内存,靠的是节点随机分布在堆里、通过指针相互连接。这也解释了为什么树不需要连续内存,以及为什么链表、树、图这些非线性结构都能存在。
坑
- 局部变量在栈上,函数返回就没了,所以不能返回指向局部变量的指针;节点必须建在堆上。
- 堆内存必须显式释放,不释放就是内存泄漏。
- 每次递归调用都会分配新的栈帧,递归深度过大就是栈溢出;递归调用的参数是副本,函数内改参数不影响调用方。
root->left = insert(...)这个赋值不能漏,它就是「把链接接回去」的那一步。- 栈的大小编译时固定,堆是运行时增长的。
请输入编辑凭据,只有站点所有者可以修改文章。