现在的位置: 首页 > 综合 > 正文

只在堆上或只在栈上定义对象

2018年12月14日 ⁄ 综合 ⁄ 共 5357字 ⁄ 字号 评论关闭
代码:

  1. class OnlyHeapClass  
  2. {  
  3. public:  
  4.     OnlyHeapClass()  
  5.     {  
  6.     }  
  7.   
  8.     void Destroy()  
  9.     {  
  10.         delete this// 等效于"OnlyHeapClass::~OnlyHeapClass();", 写  
  11.                      // 成"OnlyHeapClass::~OnlyHeapClass();"更容易理  
  12.                      // 解public成员函数调用private析构函数.  
  13.     }  
  14.   
  15. private:  
  16.     ~OnlyHeapClass()  
  17.     {  
  18.     }  
  19. };  
  20.   
  21. int main()  
  22. {  
  23.     OnlyHeapClass *pInst = new OnlyHeapClass;  
  24.   
  25.     pInst ->Destroy(); // 如果类中没有定义Destroy()函数, 而在这里用"delete pInst;"代  
  26.                        // 替"pInst->Destroy();", 则会报错. 因为"delete pInst;"会去调  
  27.                        // 用类的析构函数, 而在类域外调用类的private成员函数必然会报错.  
  28.   
  29.     return 0;  
  30. }  
总结:  把析构函数定义为private访问权限, 就可以保证只能在堆(heap)上创建(new)一个新的类对象.
    
    原因是C++是一个静态绑定的语言. 在编译过程中, 所有的非虚函数调用都必须分析完成. 即使是虚函数, 也需检查可访问性. 因些, 当在栈(stack)上生成对象时, 对象会自动析构, 也就说析构函数必须可以访问. 而堆上生成对象, 由于析构时机由程序员控制, 所以不一定需要析构函数. 保证了不能在栈上生成对象后, 需要证明能在堆上生成它. 这里OnlyHeapClass与一般对象唯一的区别在于它的析构函数为私有, delete操作会调用析构函数, 所以不能编译.

    那么如何释放它呢? 答案也很简单, 提供一个成员函数, 完成delete操作. 在成员函数中, 析构函数是可以访问的, 当然detele操作也是可以编译通过.

  1. void OnlyHeapClass::Destroy()   
  2. {   
  3.     delete this;   
  4. }   

    析构函数私有化的类的设计可以保证只能用new命令在堆(heap)中创建对象, 只能动态的去创建对象, 这样可以自由的控制对象的生命周期. 但是, 这样的类需要提供创建和撤销的公共接口. 

    另外重载delete, new为私有可以达到要求对象创建于栈上的目的, 用placement new也可以创建在栈上.

  1. ///////////////// 下面灰色字体系转载帮助理解之用 /////////////////  

 
四.禁止产生堆对象
    上面已经提到, 你决定禁止产生某种类型的堆对象, 这时你可以自己创建一个资源封装类, 该类对象只能在栈中产生, 这样就能在异常的情况下自动释放封装的资源.
    那么怎样禁止产生堆对象了? 我们已经知道, 产生堆对象的唯一方法是使用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++是如此地强大, 强大到你可以用它做你想做的任何事情. 这里主要用到的是技巧是指针类型的强制转换.    
    
  int main()   
  {   
   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对象所占的堆空间.  
    
    return 0;    
  }    

    上面的实现是麻烦的, 而且这种实现方式几乎不会在实践中使用, 但是我还是写出来路, 因为理解它, 对于我们理解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   ;   //防止使用悬挂指针     
    
    现在感觉是不是好多了, 生成对象和释放对象的操作一致了.   
【上篇】
【下篇】

抱歉!评论已关闭.