本篇博客主要介绍C++ 中动态内存相关的知识。
动态内存分配
C/C++ 定义了 4 个内存区间:
常量存储区: 存放程序代码,常量 , 不允许修改
全局变量与静态变量区: 存放全局变量,静态变量
局部变量区(栈区): 存放行数内部局部变量
动态存储区(即堆(heap)区或自由存储区(free store)): malloc,new申请的内存
内存分配方式:
从静态存储区域分配
存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
在栈上创建
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
在堆上创建
程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
堆的概念
有些操作对象只在程序运行时才能确定,这样编译时就无法为他们预定存储空间,只能在程序运行时,系统根据运行时的要求进行内存分配,这种方法称为动态存储分配。所有动态存储分配都在堆区中进行。
当程序运行到需要一个动态分配的变量或对象时,必须向系统申请取得堆中的一块所需大小的存贮空间,用于存贮该变量或对象。当不再使用该变量或对象时,也就是它的生命结束时,要显式释放它所占用的存贮空间,这样系统就能对该堆空间进行再次分配,做到重复使用有限的资源。
堆 vs 栈
首先,我们举一个例子:
1 | void f() { |
这条短短的一句话就包含了堆与栈,看到 new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
1 | 00401028 push 14h |
这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。
堆和栈究竟有什么区别?
主要的区别由以下几点:
管理方式不同
对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak.
空间大小不同
一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:
打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。
注意:
reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。能否产生碎片不同
对于堆来讲,频繁的
new/delete
势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。生长方向不同;
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式不同;
堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率不同;
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)
堆内存的分配与释放
堆空间申请、释放的方法
在C++中,申请和释放堆中分配的存贮空间,分别使用new和delete的两个运算符来完成:
1 | /* |
数组的动态内存分配
二维数组
假设我们要为一个字符数组(一个有 20 个字符的字符串)分配内存,我们可以使用上面实例中的语法来为数组动态地分配内存,如下所示:
1
2char* pvalue = NULL; // 初始化为 null 的指针
pvalue = new char[20]; // 为变量请求内存要删除我们刚才创建的数组,语句如下:
1
delete [] pvalue; // 删除 pvalue 所指向的数组
多维数组
下面是 new 操作符的通用语法,可以为多维数组分配内存,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14int ROW = 2;
int COL = 3;
double **pvalue = new double* [ROW]; // 为行分配内存
// 为列分配内存
for(int i = 0; i < COL; i++) {
pvalue[i] = new double[COL];
}
//释放多维数组内存:
for(int i = 0; i < COL; i++) {
delete[] pvalue[i];
}
delete [] pvalue;或者
1
2
3
4// new 类型名[下标表达式1] [下标表达式2]……;
//例如:建立一个动态三维数组
float (*cp)[30][20] ; //指向一个30行20列数组的指针,指向二维数组的指针
cp=new float [15] [30] [20]; //建立由15个30*20数组组成的数组;注意:cp等效于三维数组名,但没有指出其边界,即最高维的元素数量,就像指向字符的指针即等效一个字符串,不要把指向字符的指针,说成指向字符串的指针。这与数组的嵌套定义相一致。
动态一维数组的说明
- 一定注意:
delete []pc
是将n个字符的空间释放,而用delete pc
则只释放了一个字符的空间; - 如果有一个
char *pc1; pc1=p;
,同样可用delete [] pc1
来释放该空间。尽管C++不对数组作边界检查,但在堆空间分配时,对数组分配空间大小是纪录在案的。 - 没有初始化式(initializer),不可对数组初始化。
对象的动态内存分配
对象与简单的数据类型没有什么不同。
1 |
|
如果要为一个包含四个 Box 对象的数组分配内存,构造函数将被调用 4 次,同样地,当删除这些对象时,析构函数也将被调用相同的次数(4次)。
堆空间申请、释放说明:
- new 运算符返回的是一个指向所分配类型变量(对象)的指针。对所创建的变量或对象,都是通过该指针来间接操作的,而且动态创建的对象本身没有名字。
- 一般定义变量和对象时要用标识符命名,称命名对象,而动态的称无名对象(请注意与栈区中的临时对象的区别,两者完全不同:生命期不同,操作方法不同,临时变量对程序员是透明的)。
- 堆区是不会在分配时做自动初始化的(包括清零),所以必须用初始化式(initializer)来显式初始化。new表达式的操作序列如下:从堆区分配对象,然后用括号中的值初始化该对象。
1
2
3
4
5//用初始化式(initializer)来显式初始化
int *pi=new int(0);
//当pi生命周期结束时,必须释放pi所指向的目标:
delete pi;
/* 注意:这时释放了pi所指的目标的内存空间,也就是撤销了该目标,称动态内存释放(dynamic memory deallocation),但指针pi本身并没有撤销,它自己仍然存在,该指针所占内存空间并未释放。*/