类如何实现只能静态分配和只能动态分配

首先,在C++中,类对象的建立方式有两种:

  1. 静态建立类对象,如A a;这是由编译器为对象在栈空间中分配内存。
  2. 动态建立类对象,如A* p = new A;使用new运算符为对象在栈空间分配内存。

两种方式区别:

  1. 静态建立类对象:是指全局对象,静态对象,以及分配在栈区域内的对象,编译器对它们的内存分配是在编译阶段就完成了,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。
  2. 动态建立类对象:分配堆区域内的对象,编译器对他们的内存分配是在运行时动态分配的,(使用new运算符将对象建立在堆空间中。这个过程分为两步:第一步,执行operator new()函数,找到合适的内存进行分配;第二步,调用构造函数构造对象,初始化这片内存空间。使用这种方法,间接调用类的构造函数。注意这里需要寻找合适内存,然后构造,对应是析构的过程。

举个栗子:
在这里插入图片描述

注意:C++是一个静态绑定的语言。在编译过程中,所有的非虚函数调用都必须分析完成。即使是虚函数, 也需检查可访问性。因此,当在栈(stack)上生成对象时, 对象会自动析构(系统完成), 也就说析构函数必须可以访问。 而堆上生成对象,由于析构时机由程序员控制,所以不一定需要析构函数。 保证了不能在栈上生成对象后, 需要证明能在堆上生成它。这里OnlyHeapClass与一般对象唯一的区别在于它的析构函数为私有,delete操作会调用析构函数, 所以不能编译。

解决方法:提供一个成员函数, 完成delete操作。在成员函数中,析构函数是可以访问的,当然detele操作也是可以编译通过。


析构函数私有化的类的设计可以保证只能用new命令在堆(heap)中创建对象,只能动态的去创建对象, 这样可以自由的控制对象的生命周期,但这样的类需要提供创建和撤销的公共接口。
如何禁止产生堆/栈对象?
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 ;
       但是, 并不一定所有的编译器都是如此.
        既然我们可以禁止产生某种类型的堆对象, 那么可以设计一个类, 使之不能产生栈对象吗? 当然可以.


2. 禁止产生栈对象
        前面已经提到了, 创建栈对象时会移动栈顶指针以“挪出”适当大小的空间, 然后在这个空间上直接调用对应的构造函数以形成一个栈对象, 而当函数返回时, 会调用其析构函数释放这个对象, 然后再调整栈顶指针收回那块栈内存. 在这个过程中是不需要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 ; //防止使用悬挂指针

所以,记住:
静态分配:使用new运算符,对象会被建立在堆上,设new私有即可。
动态分配:将构造函数也设为private或protected
下面展示一些 内联代码片

class A
{
    
    
protected:
	A(){
    
    }
	~A(){
    
    }

public:
	static A *create()
	{
    
    
		return new A;
	}
	void destory()
	{
    
    
		delete this;
	}
};


class B
{
    
    
private:
	void* operator new(size_t t){
    
    };// 注意函数的第一个参数和返回值都是固定的
	void operator delete(void* ptr){
    
    };// 重载了new就需要重载delete
public:
	B(){
    
    };
	~B(){
    
    }
};

end-----------------------------------
参考:https://blog.csdn.net/g5dsk/article/details/4775144

猜你喜欢

转载自blog.csdn.net/qq_21197471/article/details/108693629
今日推荐