C++:在堆上创建对象,还是在栈上?
本文参考自:https://www.devbean.net/2014/02/cpp-create-object-on-heap-or-stack/ 觉得对初学者有些帮助。
关于究竟是在堆上还是在栈上创建对象,可能很多初学者感到迷惑。我想可以把这部分内容拿出来详细介绍一下。现在,假设你已经清楚什么是堆,什么是栈。
如果需要在堆上创建对象,要么使用new
运算符,要么使用malloc
系列函数。这点没有异议。
真正有异议的是下面的代码:
1 | Object obj; |
此时,obj
是在栈上分配的吗?
要回答这个问题,我们首先要理解这个语句是什么意思。这个语句就是代表着,在栈上创建对象吗?
其实,这行语句的含义是,使对象obj
具有“自动存储(automatic storage)”的性质。所谓“自动存储”,意思是这个对象的存储位置取决于其声明所在的上下文。
- 如果这个语句出现在函数内部,那么它就在栈上创建对象。
- 如果这个语句不是在函数内部,而是作为一个类的成员变量,则取决于这个类的对象是如何分配的。
考虑下面的代码:
1 | class Class |
指针pClass
所指向的对象在堆上分配空间。因为Object obj;
语句的含义是“自动存储”,所以,pClass->obj
也是在堆上创建的。
理解了这一点,再来看下面的语句:
1 | Object *pObj; |
Object *pObj;
代表,指针pObj
是自动存储的,仅此而已,没有任何其它含义。而下面一行语句则指出,这个指针所指向的对象是在堆上面分配的。如果这两行语句出现在一个函数内部,意味着当函数结束时,pObj
会被销毁,但是它指向的对象不会。因此,为了继续使用这个对象,通常我们会在函数最后添加一个return
语句,或者使用一个传出参数。否则的话,这个在堆上创建的对象就没有指针指向它,也就是说,这个对象造成了内存泄露。
并不是说指针指向的对象都是在堆上创建的。下面的代码则使用指针指向一个在栈上创建的对象:
1 | Object obj; |
至此,我们解释了函数内部的变量和成员变量。还有两类变量:全局变量和static
变量。它们即不在堆上创建,也不在栈上创建。它们有自己的内存空间,是除堆和栈以外的数据区。也就是说,当Object obj
即不在函数内部,又不是类的成员变量时,这个对象会在全局数据段创建,同理适用于static
变量。对于指针Object *pObj;
,如果这个语句出现在函数内部或类的成员变量,正如我们前面所说的,这个指针是自动存储的。但是,如果这个语句是在类的外部,它就是在全局数据段创建的。虽然它指向的对象可能在堆上创建,也可能在栈上创建。
堆和栈的区别在于两点:
- 生命周期
- 性能
第一点才是我们需要着重考虑的。由于栈的特性,如果你需要一个具有比其所在的上下文更长的生命周期的变量,只能在堆上创建它。所以,我们的推荐是:只要能在栈上创建对象,就在栈上创建;否则的话,如果你不得不需要更长的生命周期,只能选择堆上创建。这是由于在栈上的对象不需要我们手动管理内存。有经验的开发人员都会对内存管理感到头疼,我们就是要避免这种情况的发生。总的来说,我们更多推荐选择在栈上创建对象。
但是,有些情况,即便你在栈上创建了对象,它还是会占用堆的空间。考虑如下代码:
1 | void func |
对象v
是在栈上创建的。但是,STL 的vector
类其实是在堆上面存储数据的(这点可以查看源代码)。因此,只有对象v
本身是在栈上的,它所管理的数据(这些数据大多数时候都会远大于其本身的大小)还是保存在堆上。
关于第二点性能,有影响,不过一般可以忽略不计。确切的说,一般情况下你不需要考虑性能问题,除非它真的是一个问题。
首先,在堆上创建对象需要追踪内存的可用区域。这个算法是由操作系统提供,通常不会是常量时间的。当内存出现大量碎片,或者几乎用到 100% 内存时,这个过程会变得更久。与此相比,栈分配是常量时间的。其次,栈的大小是固定的,并且远小于堆的大小。所以,如果你需要分配很大的对象,或者很多很多小对象,一般而言,堆是更好的选择。如果你分配的对象大小超出栈的大小,通常会抛出一个异常。尽管很罕见,但是有时候也的确会发生。有关性能方面的问题,更多出现在嵌入式开发中:频繁地分配、释放内存可能造成碎片问题。
现代操作系统中,堆和栈都可以映射到虚拟内存中。在 32 位 Linux,我们可以把一个 2G 的数据放入堆中,而在 Mac OS 中,栈可能会限制为 65M。
总的来说,关于究竟在堆上,还是在栈上创建对象,首要考虑你所需要的生命周期。当性能真正成为瓶颈的时候,才去考虑性能的问题。堆和栈是提供给开发者的两个不同的工具,不存在一个放之四海而皆准的规则告诉你,一个对象必须放在堆中还是在栈中。选择权在开发者手中,决定权在开发者的经验中。
最后我们来补充个练习:
我们创建了一个Class Circle,这个Circle class 的头文件如下:
1 |
|
Then’s let’s check the memory address to check we the pointer locates and where the real object locates
1 |
|
The result is printed out as following:
CircleOnDataSpace’s address = 4344025352 //dataspace
ptrOnStack’s address = 140732871710304 //stack
Circle’s address = 140732871710312 //stack
Circle 1st member’s address = 140732871710312 //stack
ptr address = 140732871710296 //stack
circle_ptr’s address = 140732871710296 //stack
Circle’s address = 140471724682144 //heap
Circle’s 1st member;s address = 140471724682144 //heap
1 |