文章目录
1 堆与栈
堆与栈有数据结构和内存管理上的不同。
1.1 数据结构
栈是一种基本的数据结构,其特点是先进后出(First In Last Out)。不同于队列这种数据结构,栈只有一个出口用来放入数据与取出数据即入栈与出栈操作。入栈的数据从栈顶即出口处往栈底对数据进行入栈,出栈从栈顶依次取出数据进行出栈。利用栈可以模拟实现四则运算。
堆是计算机科学中一种特殊的完全二叉树。若满足以下特性则可称之为堆:对于堆中的任意一个节点,其父节点的值会大于等于(或者小于等于)其自身的值。堆中的任意一个节点,其父节点的值会大于等于其自身的值,则该堆被称为大根堆,若小于等于其自身的值,则为小根堆。利用堆可以实现堆排序。
1.2 内存管理
栈上的内存分布有编译器进行管理在其生命周期结束后由编译器进行回收.
在堆上分配内存需要程序员手动分配,在使用完毕之后需要及时的释放,否则将有操作系统进行回收释放占用的内存,但是程序员未及时释放很容易造成内存泄漏。
2 内存分区
关于内存到底有几个区,有说是内存四区,有说是内存五区的,都有一定的道理。这里认同的是内存五区的说法。
2.1 栈区
由编译器自动分配释放,存放函数的参数变量与局部变量的值,函数执行结束后这些存储单元自动被释放。
2.2堆区
由程序员手动分配释放,就是那些由malloc/new分配的内存块,一般都对应一个free/delete对使用完毕的内存空间进行释放,如果程序员没有释放掉,那么在程序结束后由操作系统自动回收。
2.3 全局区(静态存储区)
全局变量和静态变量被分配在同一块内存区域,在C语言中全局变量又分为初始化的和未初始化的,在C++中不再有这个区分了,它们共同占用同一块内存区域。
2.4 文字常量区
常量字符串就存放在这里,程序结束后由系统释放。
2.5代码区
存放程序代码的二进制文件。
3 堆与栈的不同:
3.1 管理方式
栈上内存是由编译器自动管理,函数执行结束后自动释放存储单元,无需手动控制。
堆上内存由程序员控制,容易造成 memory leak。
3.2 空间大小不同
堆空间和栈空间的大小是由操作系统和编程语言的实现决定的,在我的Windows10 64位系统下,堆内存空间可以达到4G空间。在 visual studio 2017 中默认的栈空间大小为1M。
3.3 是否产生碎片
对于堆来讲,频繁的 malloc/new 和 free/delete 势必会造成内存空间的不连续,从而造成大量内存碎片的产生。首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
对于栈来讲,栈是先进后出的,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,因此不会产生碎片。
3.4 生长方向
栈是由高地址向低地址即自上而下生长;
堆是由低地址向高地址即自下而上生长。
3.5 分配方式
堆都是动态分配的,没有静态分配的堆。
栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
3.6 分配效率
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持;分配专门的寄存器存放栈的地址,入栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
3.7 安全性:
在多线程编程中,堆内存是共享的,因此所有线程都可以访问、操作堆内存。多个线程同时对一块堆内存进行读写会引发竞态条件和数据不一致的问题。因此,在对多线程环境下必须采取适当的同步措施(如使用互斥锁、原子锁、线程安全的数据结构)来确保对堆内存的并发访问不会造成意外的错误和冲突。
栈内存是线程私有的,每个线程都有自己独立的占空间用来存放局部变量和函数调用信息。栈内存都不是共享的,每个线程只能访问自己的栈空间,而不能直接访问其他线程的栈。
4 堆与栈的联系
void f() {
int *p = new int[5];
}
以上一段简单的代码就包含了堆与栈,看到了关键字 new,我们首先就能想到分配一块堆内存,而指针p分配的是栈内存,f() 函数体的这一段代码就表示我们在栈内存上存放了一个指针p,指针p指向的是堆上的一块内存。
5 动态内存分配与释放
5.1 malloc/free
函数 malloc 的原型:
void *malloc(size_t size);
用malloc申请一块长度为length的整数类型的内存,程序如下:
int *p = (int*)malloc(sizeof(int) * length);
malloc 的返回值类型是 void*,所以在调用 malloc 的时候需要显示的进行类型转换,将 void* 类型显示的转化为指定的指针类型。
malloc 函数的参数列表是关于需要申请内存的大小,由于在不同位数的计算机中,内置数据类型的大小是不一样的,比如 int 变量在16位系统下是2个字节,在32位下是4个字节,因此使用 sizeof() 函数来计算指定内置数据类型所占的字节数。
函数 free 的原型:
void free(void* memblock);
为什么 free 函数不象 malloc 函数那样复杂呢?这是因为指针 p 的类型以及它所指的内存的容量事先都是知道的,语句 free 能正确地释放内存。如果 p 是 NULL 指针,那么 free 对 p 无论操作多少次都不会出问题。如果 p 不是 NULL 指针,那么 free 对 p 连续操作两次就会导致程序运行错误。
5.2 new/delete
C++ 中的内存动态分配与释放的运算符 new/delete 使用起来相对 malloc/free 简单。
int *p = new int[length];
以上是利用运算符 new 在堆上申请了一块长度为 length 的整数类型的内存,相对于用 malloc 函数申请内存的语法:int *p = (int*)malloc(sizeof(int)*length)
简单很多,因为 new 内置了 sizeof ,类型转换和类型安全检查功能。(这个地方需要学习一下运算符 new 的源码)
5.3 有了malloc/free为什么还要new/delete?
malloc/free 是 C/C++ 语言的标准库函数,new/delete 是C++的运算符,它们都可以进行堆上内存的动态分配与释放。那 有了malloc/free为什么还要new/delete?
因为对于非内部数据类型的对象而言,光用 malloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于 malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
C++语言提供的运算符 new 就是一个可以完成动态内存分配并完成对象初始化的运算符,delete 是一个可以完成释放动态内存并清理(析构)的运算符。
class Obj {
public:
Obj(void) {
std::cout << "Initialization" << std::endl;
}
~Obj(void) {
std::cout << "Destroy" << std::endl;
}
void Initialize() {
std::cout << "Initialization" << std::endl;
}
void Destroy() {
std::cout << "Destroy" << std::endl;
}
};
void testMallocFree() {
Obj *a = (Obj*)malloc(sizeof(obj)); // 动态分配内存
a->Initialize(); // 初始化
// ...
a->Destroy(); // 清除工作
free(a); // 释放内存
}
void testNewDelete() {
Obj *b = new Obj; // 申请动态内存并初始化
// ...
delete b; // 清除并且释放内存
}
以上代码类 Obj 中的函数 Initialize()
模拟了构造函数的功能,函数 Destroy()
模拟了析构函数的功能。在函数 testMallocFree()
中,由于库函数 malloc/free 不能执行构造函数和析构函数,必须调用成员函数来完成初始化与清理工作。函数 testNewDelete()
在申请动态内存的同时就会初始化,在释放动态内存之前就会完成清理工作。
对于内置的数据类型,这些对象没有构造与析构的过程,这个时候 malloc/free 与 new/delete 操作是等价的。C++程序在调用C函数时,C程序只能使用 malloc/free 来分配释放内存。其余情况下,尤其是牵涉到非内置数据类型对象(自己实现的类)的内存动态分配与释放时一律使用 new/delete。