IOS 堆、栈的使用与区别
- 数据结构
- 内存管理
- 堆、栈的区别
- 管理方式
- 体型、性能
- 存储内容
- 参考
数据结构
堆、栈是两种数据结构。
栈是一种线性的数据结构,存储和访问数据时,都只能访问栈的一端。数据访问为 FILO(先进后出)。
堆是一种特殊的二叉树,(最大堆)具有以下两个性质:
- 每个节点的值 >= 其每个子节点的值。
- 树完全平衡(任意节点的左右子树的高度差值 <= 1),最后一层的叶子节点都位于最左侧。 堆可以用树(指针节点)来实现,也可以用数组来实现:
注:可以看到,数据结构中的堆维护了一个较为复杂的结构,但实际内存中的堆几乎只是借用了堆的名字,并不会维护堆的性质与结构。
内存管理
在操作系统中,一般是指内存管理的栈区和堆区,栈区由系统自动控制,堆区是在程序中使用诸如 new/malloc
的指令进行调用的。
一个由 C/C++ 编译的程序占用的内存分为五个部分:栈区、堆区、全局区/静态区(存放全局变量或 static 静态变量)、常量区(宏或const类型)、代码区(函数体的二进制代码)。
有一个很好的例子:
int a = 0; // 全局初始化区
char *p1; // 全局未初始化区,两位在全局区的不同位置
int main () {
int b; // 栈
char s[] = "abc"; // 栈
char *p2; // 栈
char *p3 = "123456"; // 123456\0在常量区,p3在栈上。
static int c =0; // 全局(静态)初始化区
p1 = (char *)malloc(10); // 堆
p2 = (char *)malloc(20); // 堆
}
在 IOS 中(OC、Swift),堆、栈的使用与内存管理与 C 基本相同。其内存分配的示意图为:
注:栈区的大小提前分配好,然后栈指针指向栈空间地址最高处,压栈的数据向低地址区生长,出栈时只需将栈指针向高地址移动相应大小即可。
在多线程的环境下,同一进程的线程有自己完全独立的栈,但共享堆区。
堆、栈的区别管理方式
栈的空间由系统自动分配与回收,如正在运行的程序定义了一个变量 int a;
则系统自动在运行该程序的 进程/线程 的栈中开辟一个 int 整型变量的空间呈装变量 a。
堆的空间由程序进行分配与回收,如程序使用: char *p = (char *)malloc(10*sizeof(char))
获取了一个大小为 10 个 char 类型大小的空间,并由指针 p 存放空间的首地址(指令相当于创建了一个大小为 10 的 char 类型数组)。数组的数据内容在堆中,而数组的指针 p 在栈中。
程序执行完毕,指针 p 会被自动释放,但堆中的空间需要使用 free(p)
进行释放,否则将按照系统生命周期的垃圾回收机制进行处理,如果忘记手动释放,容易造成内存泄漏(堆中空间被占用,而程序中的指针已经释放,导致空间无法被访问而浪费)。
体型、性能
栈是内存上一块较小的区域,其访问方式固定(只支持从顶部添加、移除数据),运行速度较快,且没有碎片。
堆的空间比栈大得多,可在运行时动态分配内存。由于其分配的方式可任意(只要低地址处有足够的空间就可以分配,释放的时候也是直接空出对应的空间即可),因此系统需要维护一个链表来追踪堆区哪些部分被占用(以及被谁使用)。
这种块组织方式,就很容易在不断的分配和释放之中,形成小碎片(块与块之间的不足以被任何块使用的空间)。
在多线程的环境下,还需要考虑多个线程同时访问堆区的同一个块时,需要给块加锁(一致性的要求),这又是一大笔开销。
堆分配 根据实际情况不同,有许多分配策略,但其性能始终是不及栈的。
存储内容
栈一般用来存储函数参数、局部变量等数据。
在函数调用时,子函数的压栈顺序有其物理意义,而不仅仅是逻辑意义:
在函数调用时,第一个进栈的是主函数中 函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,然后是函数中的局部变量。(注意静态变量是不入栈的。)
本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序将从该点继续运行。
堆一般用来存储引用类型的对象数据。
参考
- 《C++数据结构与算法 (第4版)》 – Adam Drozdek
- iOS中堆和栈的使用
个人总结,敬请指正。