首页 C++ C++:在堆上创建对象,还是在栈上?

C++:在堆上创建对象,还是在栈上?

6 1

这篇文章来自于一次讨论:https://www.devbean.net/2013/01/qt-study-road-2-model-view/#comment-17532。关于究竟是在堆上还是在栈上创建对象,可能很多初学者感到迷惑。我想可以把这部分内容拿出来详细介绍一下。现在,假设你已经清楚什么是堆,什么是栈。

如果需要在堆上创建对象,要么使用new运算符,要么使用malloc系列函数。这点没有异议。

真正有异议的是下面的代码:

Object obj;

此时,obj是在栈上分配的吗?

要回答这个问题,我们首先要理解这个语句是什么意思。这个语句就是代表着,在栈上创建对象吗?

其实,这行语句的含义是,使对象obj具有“自动存储(automatic storage)”的性质。所谓“自动存储”,意思是这个对象的存储位置取决于其声明所在的上下文。

如果这个语句出现在函数内部,那么它就在栈上创建对象。

如果这个语句不是在函数内部,而是作为一个类的成员变量,则取决于这个类的对象是如何分配的。考虑下面的代码:

class Class
{
    Object obj;
};

Class *pClass = new Class;

指针pClass所指向的对象在堆上分配空间。因为Object obj;语句的含义是“自动存储”,所以,pClass->obj也是在堆上创建的。

理解了这一点,再来看下面的语句:

Object *pObj;
pObj = new Object;

Object *pObj;代表,指针pObj是自动存储的,仅此而已,没有任何其它含义。而下面一行语句则指出,这个指针所指向的对象是在堆上面分配的。如果这两行语句出现在一个函数内部,意味着当函数结束时,pObj会被销毁,但是它指向的对象不会。因此,为了继续使用这个对象,通常我们会在函数最后添加一个return语句,或者使用一个传出参数。否则的话,这个在堆上创建的对象就没有指针指向它,也就是说,这个对象造成了内存泄露。

并不是说指针指向的对象都是在堆上创建的。下面的代码则使用指针指向一个在栈上创建的对象:

Object obj;
Object *pObj = &obj;

至此,我们解释了函数内部的变量和成员变量。还有两类变量:全局变量和static变量。它们即不在堆上创建,也不在栈上创建。它们有自己的内存空间,是除堆和栈以外的数据区。也就是说,当Object obj即不在函数内部,又不是类的成员变量时,这个对象会在全局数据段创建,同理适用于static变量。对于指针Object *pObj;,如果这个语句出现在函数内部或类的成员变量,正如我们前面所说的,这个指针是自动存储的。但是,如果这个语句是在类的外部,它就是在全局数据段创建的。虽然它指向的对象可能在堆上创建,也可能在栈上创建。

堆和栈的区别在于两点:

  1. 生命周期
  2. 性能

第一点才是我们需要着重考虑的。由于栈的特性,如果你需要一个具有比其所在的上下文更长的生命周期的变量,只能在堆上创建它。所以,我们的推荐是:只要能在栈上创建对象,就在栈上创建;否则的话,如果你不得不需要更长的生命周期,只能选择堆上创建。这是由于在栈上的对象不需要我们手动管理内存。有经验的开发人员都会对内存管理感到头疼,我们就是要避免这种情况的发生。总的来说,我们更多推荐选择在栈上创建对象。

但是,有些情况,即便你在栈上创建了对象,它还是会占用堆的空间。考虑如下代码:

void func
{
    std::vector v;
}

对象v是在栈上创建的。但是,STL 的vector类其实是在堆上面存储数据的(这点可以查看源代码)。因此,只有对象v本身是在栈上的,它所管理的数据(这些数据大多数时候都会远大于其本身的大小)还是保存在堆上。

关于第二点性能,有影响,不过一般可以忽略不计。确切的说,一般情况下你不需要考虑性能问题,除非它真的是一个问题。

首先,在堆上创建对象需要追踪内存的可用区域。这个算法是由操作系统提供,通常不会是常量时间的。当内存出现大量碎片,或者几乎用到 100% 内存时,这个过程会变得更久。与此相比,栈分配是常量时间的。其次,栈的大小是固定的,并且远小于堆的大小。所以,如果你需要分配很大的对象,或者很多很多小对象,一般而言,堆是更好的选择。如果你分配的对象大小超出栈的大小,通常会抛出一个异常。尽管很罕见,但是有时候也的确会发生。有关性能方面的问题,更多出现在嵌入式开发中:频繁地分配、释放内存可能造成碎片问题。

现代操作系统中,堆和栈都可以映射到虚拟内存中。在 32 位 Linux,我们可以把一个 2G 的数据放入堆中,而在 Mac OS 中,栈可能会限制为 65M。

总的来说,关于究竟在堆上,还是在栈上创建对象,首要考虑你所需要的生命周期。当性能真正成为瓶颈的时候,才去考虑性能的问题。堆和栈是提供给开发者的两个不同的工具,不存在一个放之四海而皆准的规则告诉你,一个对象必须放在堆中还是在栈中。选择权在开发者手中,决定权在开发者的经验中。

6 评论

panda 2014年2月22日 - 10:51

在堆上创建对象后,清除工作什么时候可以托管(父组件),什么时候必须自己删?

回复
豆子 2014年2月22日 - 15:43

父组件托管仅适用于 Qt,只要是 QObject 的子类,都有支持该机制。个人意见,只要能够给出 parent 指针,就给出该指针,让 Qt 去管理内存。这么做还是为了减少开发人员的负担。

回复
大灰狼嘎嘎 2014年3月2日 - 17:40

零零散散说了好多啊,恐怕初学者看了会望而生畏把。。哈哈
那天我也想了一下,想出了一个简单的准则,初学者可以试着采用:
1. 各种非容器类(如窗口类、网络相关的等等)尽量的用new来创立。
补充:如果生命周期和父组建一样的话,就 Object *obj = new Object(this); 如果生命周期可能长于父组件的话,就Object *obj = new Object;
理由:每个进程给的栈空间都不大,那么可能大量占用内存的对象还是放到堆中好了。(反正性能上的差异可以忽略 :) )

2. 各种常用容器类(如QString、QList等)尽量的不用new来创立。(我可没说建在栈上哦~)
理由:①一般情况下容器类的占用内存都不大,而且离开上下文之后就释放掉了。
②使用方便,毕竟要用个QString的话,如果还要QString *string = new QString(this) 的话太麻烦了。

该准则基于以下前提:
1. 初学者不了解一个容器类的内部实现,到底是占用的堆空间还是栈空间。
2. QT的父组件机制能兼顾 动态内存申请 和 自动释放。所以如果是在其他C++框架下这个准则就不一定适用了。

回复
大灰狼嘎嘎 2014年3月2日 - 17:42

豆哥你觉得呢?

回复
kidddddd1984 2014年7月24日 - 20:46

豆子大大好,想问一下例子中出现的这种情况
class Class
{
Object obj;
};
Class *pClass = new Class;

因为pClass创建在堆上所以obj也是创建在了堆上,那么在pClass析构的时候,obj所占用的内存会自动被释放掉么。如果考虑obj的生命周期不超过自己所在类的生命周期的话,应该会自动释放掉吧。

回复
豆子 2014年7月25日 - 23:42

是的,当 delete pClass 的时候,指针指向的对象就会被析构,包括其成员。

回复

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2