C++——内存对象 禁止产生堆对象 禁止产生栈对象

 

用C或C++写程序,须要更多地关注内存,这不只仅是由于内存的分配是否合理直接影响着程序的效率和性能,更为主要的是,当咱们操做内存的时候一不当心就会出现问题,并且不少时候,这些问题都是不易发觉的,好比内存泄漏,好比悬挂指针。程序员

咱们知道,C++将内存划分为三个逻辑区域:堆、栈和静态存储区。既然如此,我称位于它们之中的对象分别为堆对象,栈对象以及静态对象。那么这些不一样的内存对象有什么区别了?堆对象和栈对象各有什么优劣了?如何禁止建立堆对象或栈对象了?算法

一.基本概念
先来看看栈。
栈,通常用于存放局部变量或对象,如咱们在函数定义中用相似下面语句声明的对象:
Type stack_object ;
stack_object即是一个栈对象,它的生命期是从定义点开始,当所在函数返回时,生命结束。

另外,几乎全部的临时对象都是栈对象。好比,下面的函数定义:
Type fun(Type object) ;

这个函数至少产生两个临时对象,首先,参数是按值传递的,因此会调用拷贝构造函数生成一个临时对象object_copy1,在函数内部使用的不是使用的不是object,而是object_copy1,天然,object_copy1是一个栈对象,它在函数返回时被释放;还有这个函数是值返回的,在函数返回时,若是咱们不考虑返回值优化(NRV),那么也会产生一个临时对象object_copy2,这个临时对象会在函数返回后一段时间内被释放。好比某个函数中有以下代码:
Type tt ,result ; //生成两个栈对象
tt = fun(tt) ; //函数返回时,生成的是一个临时对象object_copy2

上面的第二个语句的执行状况是这样的,首先函数fun返回时生成一个临时对象object_copy2 ,而后再调用赋值运算符执行
tt = object_copy2 ; //调用赋值运算符
看到了吗?编译器在咱们毫无知觉的状况下,为咱们生成了这么多临时对象,而生成这些临时对象的时间和空间的开销多是很大的,因此,你也许明白了,为何对于“大”对象最好用const引用传递代替按值进行函数参数传递了(const引用传递时,函数内部直接进行操做的是这个const引用自己本不须要进行变量的拷贝)。设计模式

接下来,看看堆。
堆,又叫自由存储区,它是在程序执行的过程当中动态分配的,因此它最大的特性就是动态性。在C++中,全部堆对象的建立和销毁都要由程序员负责,因此,若是处理很差,就会发生内存问题。若是分配了堆对象,却忘记了释放,就会产生内存泄漏;而若是已释放了对象,却没有将相应的指针置为NULL,该指针就是所谓的“悬挂指针”,再度使用此指针时,就会出现非法访问,严重时就致使程序崩溃。
那么,C++中是怎样分配堆对象的?惟一的方法就是用new(固然,用类malloc指令也可得到C式堆内存),只要使用new,就会在堆中分配一块内存,而且返回指向该堆对象的指针。安全

再来看看静态存储区。
全部的静态对象、全局对象都于静态存储区分配。关于全局对象,是在main()函数执行前就分配好了的。其实,在main()函数中的显示代码执行以前,会调用一个由编译器生成的_main()函数,而_main()函数会进行全部全局对象的的构造及初始化工做。而在main()函数结束以前,会调用由编译器生成的exit函数,来释放全部的全局对象。好比下面的代码:
void main(void)
{
… …// 显式代码
}

实际上,被转化成这样:
void main(void)
{
_main(); //隐式代码,由编译器产生,用以构造全部全局对象
… … // 显式代码
… …
exit() ; // 隐式代码,由编译器产生,用以释放全部全局对象
}

因此,知道了这个以后,即可以由此引出一些技巧,如,假设咱们要在main()函数执行以前作某些准备工做,那么咱们能够将这些准备工做写到一个自定义的全局对象的构造函数中,这样,在main()函数的显式代码执行以前,这个全局对象的构造函数会被调用,执行预期的动做,这样就达到了咱们的目的。
刚才讲的是静态存储区中的全局对象,那么,局部静态对象了?局部静态对象一般也是在函数中定义的,就像栈对象同样,只不过,其前面多了个static关键字。局部静态对象的生命期是从其所在函数第一次被调用,更确切地说,是当第一次执行到该静态对象的声明代码时,产生该静态局部对象,直到整个程序结束时,才销毁该对象。函数

还有一种静态对象,那就是它做为class的静态成员。考虑这种状况时,就牵涉了一些较复杂的问题。布局

第一个问题是class的静态成员对象的生命期,class的静态成员对象随着第一个class object的产生而产生,在整个程序结束时消亡。也就是有这样的状况存在,在程序中咱们定义了一个class,该类中有一个静态对象做为成员,可是在程序执行过程当中,若是咱们没有建立任何一个该class object,那么也就不会产生该class所包含的那个静态对象。还有,若是建立了多个class object,那么全部这些object都共享那个静态对象成员。性能

第二个问题是,当出现下列状况时:
class Base{
public:
static Type s_object ;
}
class Derived1 : public Base{ // 公共继承
… …// other data
}
class Derived2 : public Base{ // 公共继承
… …// other data
}优化

Base example ;
Derivde1 example1 ;
Derivde2 example2 ;
example.s_object = …… ;
example1.s_object = …… ;
example2.s_object = …… ;

请注意上面最后的三条语句,它们所访问的s_object是同一个对象吗?答案是确定的,它们的确是指向同一个对象,这听起来不像是真的,是吗?但这是事实,你能够本身写段简单的代码验证一下。
我要作的是来解释为何会这样? 咱们知道,当一个类好比Derived1,从另外一个类好比Base继承时,那么,能够看做一个Derived1对象中含有一个Base型的对象,这就是一个subobject。this

让咱们想一想,当咱们将一个Derived1型的对象传给一个接受非引用Base型参数的函数时会发生切割,那么是怎么切割的呢?相信如今你已经知道了,那就是仅仅取出了Derived1型的对象中的subobject,而忽略了全部Derived1自定义的其它数据成员,而后将这个subobject传递给函数(实际上,函数中使用的是这个subobject的拷贝)。设计

全部继承Base类的派生类的对象都含有一个Base型的subobject(这是能用Base型指针指向一个Derived1对象的关键所在,天然也是多态的关键了),而全部的subobject和全部Base型的对象都共用同一个s_object对象,天然,从Base类派生的整个继承体系中的类的实例都会共用同一个s_object对象了。



二.三种内存对象的比较

栈对象的优点是在适当的时候自动生成,又在适当的时候自动销毁,不须要程序员操心;并且栈对象的建立速度通常较堆对象快,由于分配堆对象时,会调用operator new操做,operator new会采用某种内存空间搜索算法,而该搜索过程多是很费时间的,产生栈对象则没有这么麻烦,它仅仅须要移动栈顶指针就能够了。可是要注意的是,一般栈空间容量比较小,通常是1MB~2MB,因此体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,由于随着递归调用深度的增长,所需的栈空间也会线性增长,当所需栈空间不够时,便会致使栈溢出,这样就会产生运行时错误。

堆对象,其产生时刻和销毁时刻都要程序员精肯定义,也就是说,程序员对堆对象的生命具备彻底的控制权。咱们经常须要这样的对象,好比,咱们须要建立一个对象,可以被多个函数所访问,可是又不想使其成为全局的,那么这个时候建立一个堆对象无疑是良好的选择,而后在各个函数之间传递这个堆对象的指针,即可以实现对该对象的共享。另外,相比于栈空间,堆的容量要大得多。实际上,当物理内存不够时,若是这时还须要生成新的堆对象,一般不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。

接下来看看static对象。
首先是全局对象。全局对象为类间通讯和函数间通讯提供了一种最简单的方式,虽然这种方式并不优雅。通常而言,在彻底的面向对象语言中,是不存在全局对象的,好比C#,由于全局对象意味着不安全和高耦合,在程序中过多地使用全局对象将大大下降程序的健壮性、稳定性、可维护性和可复用性。C++也彻底能够剔除全局对象,可是最终没有,我想缘由之一是为了兼容C。
其次是类的静态成员,上面已经提到,基类及其派生类的全部对象都共享这个静态成员对象,因此当须要在这些class之间或这些class objects之间进行数据共享或通讯时,这样的静态成员无疑是很好的选择。
接着是静态局部对象,主要可用于保存该对象所在函数被多次调用期间的中间状态,其中一个最显著的例子就是递归函数,咱们都知道递归函数是本身调用本身的函数,若是在递归函数中定义一个nonstatic局部对象,那么当递归次数至关大时,所产生的开销也是巨大的。这是由于nonstatic局部对象是栈对象,每递归调用一次,就会产生一个这样的对象,每返回一次,就会释放这个对象,并且,这样的对象只局限于当前调用层,对于更深刻的嵌套层和更浅露的外层,都是不可见的。每一个层都有本身的局部对象和参数。在递归函数设计中,可使用static对象替代nonstatic局部对象(即栈对象),这不只能够减小每次递归调用和返回时产生和释放nonstatic对象的开销,并且static对象还能够保存递归调用的中间状态,而且可为各个调用层所访问。



三.使用栈对象的意外收获

前面已经介绍到,栈对象是在适当的时候建立,而后在适当的时候自动释放的,也就是栈对象有自动管理功能。那么栈对象会在何时自动释放了?第一,在其生命期结束的时候;第二,在其所在的函数发生异常的时候。你也许说,这些都很正常啊,没什么大不了的。是的,没什么大不了的。可是只要咱们再深刻一点点,也许就有意外的收获了。

栈对象,自动释放时,会调用它本身的析构函数。若是咱们在栈对象中封装资源,并且在栈对象的析构函数中执行释放资源的动做,那么就会使资源泄漏的几率大大下降,由于栈对象能够自动的释放资源,即便在所在函数发生异常的时候。
实际的过程是这样的:函数抛出异常时,会发生所谓的stack_unwinding(堆栈回滚),即堆栈会展开,因为是栈对象,天然存在于栈中,因此在堆栈回滚的过程当中,栈对象的析构函数会被执行,从而释放其所封装的资源。除非,除非在析构函数执行的过程当中再次抛出异常――而这种可能性是很小的,因此用栈对象封装资源是比较安全的。基于此认识,咱们就能够建立一个本身的句柄或代理来封装资源了。智能指针(auto_ptr)中就使用了这种技术。在有这种须要的时候,咱们就但愿咱们的资源封装类只能在栈中建立,也就是要限制在堆中建立该资源封装类的实例。


四.禁止产生堆对象

上面已经提到,你决定禁止产生某种类型的堆对象,这时你能够本身建立一个资源封装类,该类对象只能在栈中产生,这样就能在异常的状况下自动释放封装的资源。

那么怎样禁止产生堆对象了?咱们已经知道,产生堆对象的惟一方法是使用new操做,若是咱们禁止使用new不就好了么。再进一步,new操做执行时会调用operator new,而operator new是能够重载的。方法有了,就是使new operator 为private,为了对称,最好将operator delete也重载为private。如今,你也许又有疑问了,难道建立栈对象不须要调用new吗?是的,不须要,由于建立栈对象不须要搜索内存,而是直接调整堆栈指针,将对象压栈,而operator new的主要任务是搜索合适的堆内存,为堆对象分配空间,这在上面已经提到过了。好,让咱们看看下面的示例代码:

#include <stdlib.h> //须要用到C式内存分配函数
class Resource ; //表明须要被封装的资源类
class NoHashObject {
private:
Resource* ptr ;//指向被封装的资源
... ... //其它数据成员
void* operator new(size_t size){ //非严格实现,仅做示意之用
return malloc(size) ;
}
void operator delete(void* pp){ //非严格实现,仅做示意之用
free(pp) ;
}
public:
NoHashObject(){
//此处能够得到须要封装的资源,并让ptr指针指向该资源
ptr = new Resource() ;
}
~NoHashObject(){
delete ptr ; //释放封装的资源
}
};

NoHashObject如今就是一个禁止堆对象的类了,若是你写下以下代码:
NoHashObject* fp = new NoHashObject() ; //编译期错误!
delete fp ;

上面代码会产生编译期错误。好了,如今你已经知道了如何设计一个禁止堆对象的类了,你也许和我同样有这样的疑问,难道在类NoHashObject的定义不能改变的状况下,就必定不能产生该类型的堆对象了吗?不,仍是有办法的,我称之为“暴力破解法”。C++是如此地强大,强大到你能够用它作你想作的任何事情。这里主要用到的是技巧是指针类型的强制转换。
void main(void) {
char* temp = new char[sizeof(NoHashObject)] ;

//强制类型转换,如今ptr是一个指向NoHashObject对象的指针
NoHashObject* obj_ptr = (NoHashObject*)temp ;

temp = NULL ; //防止经过temp指针修改NoHashObject对象

//再一次强制类型转换,让rp指针指向堆中NoHashObject对象的ptr成员
Resource* rp = (Resource*)obj_ptr ;

//初始化obj_ptr指向的NoHashObject对象的ptr成员
rp = new Resource() ;
//如今能够经过使用obj_ptr指针使用堆中的NoHashObject对象成员了
... ...

delete rp ;//释放资源
temp = (char*)obj_ptr ;
obj_ptr = NULL ;//防止悬挂指针产生
delete [] temp ;//释放NoHashObject对象所占的堆空间。
}

上面的实现是麻烦的,并且这种实现方式几乎不会在实践中使用,可是我仍是写出来路,由于理解它,对于咱们理解C++内存对象是有好处的。
对于上面的这么多强制类型转换,其最根本的是什么了?咱们能够这样理解:
某块内存中的数据是不变的,而类型就是咱们戴上的眼镜,当咱们戴上一种眼镜后,咱们就会用对应的类型来解释内存中的数据,这样不一样的解释就获得了不一样的信息。
所谓强制类型转换实际上就是换上另外一副眼镜后再来看一样的那块内存数据。

另外要提醒的是,不一样的编译器对对象的成员数据的布局安排多是不同的,好比,大多数编译器将NoHashObject的ptr指针成员安排在对象空间的头4个字节,这样才会保证下面这条语句的转换动做像咱们预期的那样执行:
Resource* rp = (Resource*)obj_ptr ;

可是,并不必定全部的编译器都是如此。
既然咱们能够禁止产生某种类型的堆对象,那么能够设计一个类,使之不能产生栈对象吗?固然能够。



五.禁止产生栈对象

前面已经提到了,建立栈对象时会移动栈顶指针以“挪出”适当大小的空间,而后在这个空间上直接调用对应的构造函数以造成一个栈对象,而当函数返回时,会调用其析构函数释放这个对象,而后再调整栈顶指针收回那块栈内存。在这个过程当中是不须要operator new/delete操做的,因此将operator new/delete设置为private不能达到目的。固然从上面的叙述中,你也许已经想到了:将构造函数或析构函数设为私有的,这样系统就不能调用构造/析构函数了,固然就不能在栈中生成对象了。

这样的确能够,并且我也打算采用这种方案。可是在此以前,有一点须要考虑清楚,那就是,若是咱们将构造函数设置为私有,那么咱们也就不能用new来直接产生堆对象了,由于new在为对象分配空间后也会调用它的构造函数啊。因此,我打算只将析构函数设置为private。再进一步,将析构函数设为private除了会限制栈对象生成外,还有其它影响吗?是的,这还会限制继承。

若是一个类不打算做为基类,一般采用的方案就是将其析构函数声明为private。

为了限制栈对象,却不限制继承,咱们能够将析构函数声明为protected,这样就一箭双鵰了。以下代码所示:
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//调用保护析构函数
}
};

接着,能够像这样使用NoStackObject类:
NoStackObject* hash_ptr = new NoStackObject() ;
... ... //对hash_ptr指向的对象进行操做
hash_ptr->destroy() ;

呵呵,是否是以为有点怪怪的,咱们用new建立一个对象,却不是用delete去删除它,而是要用destroy方法。很显然,用户是不习惯这种怪异的使用方式的。因此,我决定将构造函数也设为private或protected。这又回到了上面曾试图避免的问题,即不用new,那么该用什么方式来生成一个对象了?咱们能够用间接的办法完成,即让这个类提供一个static成员函数专门用于产生该类型的堆对象。(设计模式中的singleton模式就能够用这种方式实现。)让咱们来看看:
class NoStackObject
{
protected:
NoStackObject() { }
~NoStackObject() { }
public:
static NoStackObject* creatInstance() {
return new NoStackObject() ;//调用保护的构造函数
}
void destroy() {
delete this ;//调用保护的析构函数
}
};

如今能够这样使用NoStackObject类了:
NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
... ... //对hash_ptr指向的对象进行操做
hash_ptr->destroy() ;
hash_ptr = NULL ; //防止使用悬挂指针

如今感受是否是好多了,生成对象和释放对象的操做一致了。

ok,讲到这里,已经涉及了较多的东西,若是要把内存对象讲得更深刻更全面,那可能须要写成一本书了,而就我本身的功力而言,多是很难彻底把握的。若是上面所写的能使你有所收获或启发,我就知足了。若是你要更进一步去了解内存对象方面的知识,那么我能够推荐你看看《深刻探索C++对象模型》这本书。

相关文章
相关标签/搜索