一、内存相关的堆、栈
首先,讨论的堆和栈指的是内存中的“堆区”和“栈区”。
1、C语言的内存模型分为5个区:栈区、堆区、静态区、常量区、代码区。每个区存储的内容如下:
-
栈区:存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,其操作方式类似于数据结构中的栈。栈内存分配运算内置于CPU的指令集,效率很高,但是分配的内存量有限,比如iOS中栈区的大小是2M。
-
堆区:就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序区释放。分配方式类似于数据结构中的链表。在iOS开发中所说的“内存泄漏”说的就是堆区的内存。
-
静态区:全局变量和静态变量(在iOS中就是用static修饰的局部变量或者是全局全局变量)的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。
-
常量区:常量存储在这里,不允许修改。
-
代码区:存放函数体的二进制代码。
int a = 0; 全局初始化区
char *p1; 全局未初始化区
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);
分配得来得10和20字节的区域就在堆 区。
strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"
优 化成一个地方。
}
2、区别
- 1.管理方式不同,系统控制与程序员控制。
栈由操作系统自动分配释放,无需我们手动控制;
堆的申请和释放工作由程序员控制,容易产生内存泄漏
- 2.空间大小不同,栈小而堆大。
每个进程拥有的栈的大小要远远小于堆的大小。
理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小64bits的Windows默认1M,64bits的Linux默认10M
- 3.生长方向不同,堆自顶向上,而栈则相反。
堆的生长方向向上,内存地址由低到高;
栈的生长方向向下,内存地址由高到低。
- 4.分配方式不同,栈是动静态分配,堆只能动态。
堆都是动态分配的,没有静态分配的堆。
栈有2种分配方式:静态分配和动态分配。
静态分配是由操作系统完成的,比如局部变量的分配。动态分配由malloc或new函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。
- 5.分配效率不同,栈快而堆慢
栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
- 6.存放内容不同,栈存放参数、变量、地址,堆存放对象。
栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。
当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内存(eip),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(ebp),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是调用函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。
堆存放的内容,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。
- 7.生命周期不同,栈短而堆长。
栈中存储的数据的生命周期随着函数的执行完成而结束。因为它会被系统自动释放。
堆中存储的数据的若未释放,则其生命周期等同于程序的生命周期。
- 8.申请后系统的响应如何?
栈:无需申请,系统将为程序提供内存,或将报异常提示栈溢出。
堆:关于堆上内存空间的分配过程,首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样主动释放时才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
- 9.是否产生内存碎片?
堆:由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。
栈:在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。
虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。
无论是堆还是栈,在内存使用时都要防止非法越界,越界导致的非法内存访问可能会摧毁程序的堆、栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。
二、数据结构相关的堆、栈
1.栈
栈是一种线性结构,所以可以使用数组或链表(单向链表、双向链表或循环链表)作为底层数据结构。
栈分顺序栈和链式栈两种。
使用数组实现的栈叫做顺序栈,使用链表实现的栈叫做链式栈,二者的区别是顺序栈中的元素地址连续,链式栈中的元素地址不连续。
- 栈的特性是什么?
“先进后出”(First In Last Out),简称FILO
- 构建一个栈需要什么元素?
一块连续内存,比如数组。
一个指针,指向空内存以备PUSH操作,简称SP。
- 栈有哪些操作?
push、pop、isEmpty
2.堆(优先队列)
堆是一种常用的树形结构,是一种特殊的完全二叉树。当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆。堆的这一特性称之为堆序性。因此,在一个堆中,根节点是最大(或最小)节点。如果根节点最小,称之为小顶堆(或小根堆),如果根节点最大,称之为大顶堆(或大根堆)。堆的左右孩子没有大小的顺序。
- 堆的特性是什么?
当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆。堆的这一特性称之为堆序性。
- 堆有哪些操作?
build(建立堆)、adjust(调整堆)、pop(弹出堆顶元素)、insert(往堆底插入元素)