> 文章列表 > 【C++】特殊类的设计 + 单例模式

【C++】特殊类的设计 + 单例模式

【C++】特殊类的设计 + 单例模式

文章目录

  • 📖 前言
  • 1. 特殊类的设计
    • 1.1 设计一个类,不能被拷贝:
    • 1.2 设计一个类,只能在堆上创建对象
      • 🏁方法一:
      • 🏁方法二:
    • 1.3 设计一个类,只能在栈上面创建对象:
      • 📝方法一:
      • 📝方法二:
    • 1.4 请设计一个类,不能被继承:
  • 2. 单例模式:
    • 2.1 饿汉模式:
    • 2.2 懒汉模式:

📖 前言

在我们的学习中不免会遇到一些要设计一些特殊的类,要求这些类只能在内存中特定的位置创建对象,这就需要我们对类进行一些特殊的处理,那我们该如何解决呢?下面来絮叨絮叨~~🙋🙋🙋🙌


1. 特殊类的设计

1.1 设计一个类,不能被拷贝:

拷贝只会放生在两个场景中:

  • 拷贝构造函数以及赋值运算符重载
  • 因此想要让一个类禁止拷贝
  • 只需让该类不能调用拷贝构造函数以及赋值运算符重载即可
  • C++98的方式,既然不能让拷贝我们只需要将拷贝构造和赋值重载设成私有即可。

具体代码实现:

class CopyBan
{// ...
private:CopyBan(const CopyBan&);CopyBan& operator=(const CopyBan&);
};
  1. 设置成私有: 如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了(老六会在类外面实现,如果不设置成私有的情况)
  2. 只声明不定义: 不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了
  • C++11扩展delete的用法,delete除了释放new申请的资源外
  • 如果在默认成员函数后跟上 = delete,表示让编译器删除掉该默认成员函数。
class CopyBan
{// ...CopyBan(const CopyBan&) = delete;CopyBan& operator=(const CopyBan&) = delete;// ...
};

1.2 设计一个类,只能在堆上创建对象:

🏁方法一:

分析题目既然只能将对象创建在堆上,那么我们就需要将在栈上或数据段创建对象的这种情况给屏蔽掉。

首先我们我们的思路:

  • 既然不能随便的创建出对象
  • 我们就直接将构造函数私有化
  • 这时就直接将三路封死了(栈,堆,数据段上创建对象)

下面我们要做的就是要单独开道口子给在堆上创建对象用~~

虽然我们在类外用不了构造函数,但是我们可以在类内调用,所以我们在类内调用一下构造函数,并将创建的对象返回出来。

class HeapOnly
{
public:static HeapOnly* CreateObj(){//这里new直接去调用构造函数去了,类里面直接调用没限制return new HeapOnly;}//防拷贝,不让拷贝(形参可写可不写)HeapOnly(const HeapOnly&) = delete;//赋值并不会影响创建的对象跑到其他的地方去了,赋值不是创建对象的//拷贝构造反而是用来创建对象的private://构造函数私有 -- 将构造函数封起来(三条路都封死)HeapOnly(){}
};

问题:

  • 那我们在类外调用CreateObj()来创建对象的时候有个很尬的问题
  • 那就是著名的先有鸡还是先有蛋的问题
  • 要调用成员函数,就要有对象,要有对象,就要调用成员函数,要调用成员函数……
  • 我们要解决这个问题就必须打破这个循环死穴
  • 我们将CreateObj()函数设成静态函数

补充:

  • 普通的非静态的构造函数都需要对象去调用
  • 而构造函数不需要
  • 静态的成员函数没有this指针,不能访问常见的成员,但是可以访问构造函数。
  • 因为构造函数不用this指针调用,并不是说构造函数没有this指针。

此时还有个问题就是,拷贝构造也是会在栈上创建对象,我们可以将拷贝构造设成私有,或者C++11中直接将拷贝构造给删除掉。

通过类域直接调用构造函数:

int main()
{//HeapOnly h1;//栈//static HeapOnly h2;//静态区//HeapOnly* ph3 = new HeapOnly;//堆//此时无论如何创建的对象都是在堆上的HeapOnly* ph4 = HeapOnly::CreateObj();HeapOnly* ph5 = HeapOnly::CreateObj();//创建在堆上,但是拷贝还是拷贝在了栈上//HeapOnly copy(*ph4);delete ph4;delete ph5;return 0;
}

🏁方法二:

相比于上一种方法(将构造函数和拷贝构造私有或者删除),方法二显得更加牵强一点,将析构函数设成私有的,这样当对象生命周期结束时自动调用析构函数时会调用不到。

首先我们我们的思路:

  • 首先我们知道不在堆上创建的对象不用通过手动释放空间
  • 会在对象生命周期结束的时候自动调用其析构函数
  • 我们将析构函数设成私有就导致其不能正常析构
  • 编译器会报错
    在这里插入图片描述
  • 析构函数私有间接的将全局的,静态的,局部的对象都给禁掉了,唯独将把new给留下了
  • 这种方式的缺陷是:new的时候确实能创建出来,但是在delete释放的时候会报错

解决办法:类外调用不了私有的析构函数,但是类内可以调用,所以我们可以提供一个接口

class HeapOnly
{
public:static void DelObj(HeapOnly* ptr){delete ptr;}//比较花的玩法 -- 这样还不用传参了/*void DelObj(){delete this; }*/private://析构函数私有~HeapOnly(){}
};int main()
{//HeapOnly h1;//static HeapOnly h2;HeapOnly* ph3 = new HeapOnly;//delete ph3;//方法一:要传参ph3->DelObj(ph3);//方法二:不用传参//ph3->DelObj();return 0;
}

还有个比较花的玩法,直接在类内将this指针释放掉,这种写法也没问题,就是很少见。


1.3 设计一个类,只能在栈上面创建对象:

📝方法一:

类似于上一个问题中的方法,我们先将三条路封死,然后单独给在栈上创建对象开条出路。

首先我们我们的思路:

  • 先将构造函数私有化
  • 然后在类内写一个函数专门用来创建对象并负责将其传出来
  • 注意:此时传返回值只能是传值返回,不能传指针,也不能传引用
  • 因为是在栈上创建对象,是个局部变量,出了作用域就销毁掉了
class StackOnly
{
public:static StackOnly CreateObj(){//局部对象,不能用指针返回,也不能用引用返回,只能传值返回//传值返回必然会有一个拷贝构造发生return StackOnly();}void Print(){cout << "Stack Only" << endl;}
private://构造函数私有StackOnly(){}
};int main()
{StackOnly h1 = StackOnly::CreateObj();//不用对象去接受StackOnly::CreateObj().Print();//static StackOnly h2;//禁掉下面的玩法//StackOnly* ph3 = new StackOnly;return 0;
}

和上一个题不同的地方,这里不能将拷贝构造给禁掉,因为CreateObj()是传值返回,传值返回必然会有一个拷贝构造(不考虑编译器优化的问题)。

📝方法二:

再来个比较花的玩法,我们要禁掉在堆上创建对象的情况,所以我们可以对new下手。

之前我们学习C++内存管理时,我们知道new一共分为两个步骤:开空间 + 调用构造函数。

  • 首先new是个操作符,负责空间家 + 初始化的
  • new在底层会调用operator new
  • 内存管理运算符new、new[]、delete 和 delete[]也可以进行重载
  • 其重载形式既可以是类的成员函数,也可以是全局函数
  • 一般情况下,内建的内存管理运算符就够用了,只有在需要自己管理内存时才会重载

补充:

  • 类专属的operator new函数属于new操作符的重载。它提供了一种自定义内存分配策略的方法,用于为该类的对象分配内存空间。而全局的operator new函数则为所有类型的对象提供默认的内存分配策略。
  • 编译器会在类中查找是否有声明或定义了类专属的operator new函数。如果找到了,则编译器会使用该函数来分配对象的内存空间。如果没有找到,编译器会继续往父类查找是否存在类专属的operator new函数,直到找到一个已经声明或定义了该函数的类或者到达继承体系的根部为止。
  • 即使某个类没有定义类专属的operator new函数,编译器仍然可以使用全局的operator new函数进行内存分配。因此,类专属的operator new函数并不是必需的,但它可以提供一种更加灵活和精细的内存分配策略,以满足特定的需求。

在具备上述知识体系之后,我们在类内实现这个类专属的operator new,并且我们直接将其删掉,这样在堆上创建对象这一个操作就执行不了了。

class StackOnly
{
public://new由两部分构成,一部分调用operator new,另一部分是调用构造函数//直接把operator new给禁掉,不禁构造函数//类内专属重载了,一个类的专属的operator new,并且将其删掉void* operator new(size_t size) = delete;void operator delete(void* p) = delete;
};//全局的也禁不掉
StackOnly s3;int main()
{//new会自动调用operator new//这里的new就不会去调用全局的,而是调用类里的//此时类内的operator new 被删掉了,就调用不到了//StackOnly* p1 = new StackOnly;//StackOnly s1;//静态的和全局的禁不掉static StackOnly s2;return 0;
}

不过还是有的情况防不住,那就是在数据段(静态对象和全局对象)禁不掉。


1.4 请设计一个类,不能被继承:

这个内容我们在继承那一节中详细讲过,不熟悉的小伙办可以回去复习哦😁,👉 传送门


2. 单例模式:

概念:

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,一个进程只能有一个对象,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

2.1 饿汉模式:

  • 这种模式是不管你将来用不用这个类的对象,程序启动时就创建一个唯一的实例对象。
class Singleton
{
public:static Singleton* GetInstance(){cout << _spInst << endl;return _spInst;}private:Singleton(){}//防不住拷贝,直接将其删除Singleton(const Singleton&) = delete;static Singleton _sInst; //声明static Singleton* _spInst; //声明//饿汉模式初始化一个成员int _a = 0;
};Singleton Singleton::_sInst; //定义
Singleton* Singleton::_spInst = new Singleton; //定义int main()
{//GetInstance() 可以或者这个Singleton类的单例对象Singleton::GetInstance()->Print();//在外面就定义不出来对象了//Singleton st1;//Singleton* st2 = new Singleton;//拷贝删除掉//Singleton copy(*Singleton::GetInstance());return 0;
}

思路:

  • 饿汉 — 一开始(main函数之前)就创建好了
  • 饿汉是指,在main函数之前就已经创建好对象了
  • 全局对象和静态对象就是在main函数之前创建的

我们将拷贝构造给禁掉了:

  • 目的就是防止通过拷贝构造创建出一个新的对象出来。

注意:

  • 创建一个静态对象,静态的对象只是收到这个类域的限制(并不是对象真正的成员)
  • 也就是说Singleton这个类实例化的对象中没有这个对象
  • 静态的不属于某个对象,它是属于所有对象,属于整个类

饿汉,对象在main函数之前就创建了,导致了程序启动起来很慢。


2.2 懒汉模式:

  • 一开始不创建对象,在第一调用GetInstance()的时候再创建对象。

如果单例对象构造十分耗时或者占用很多资源,比如加载插件, 初始化网络连接,读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。

class InfoMgr
{
public:static InfoMgr* GetInstance(){//还需要加锁,这个后面讲  -- 双检查加锁if (_spInst == nullptr){_spInst = new InfoMgr;}return _spInst;}void SetAddress(const string& s){_address = s;}string& GetAddress(){return _address;}//实现一个内嵌垃圾回收类    class CGarbo {public:~CGarbo() {if (_spInst){delete _spInst;}}};//定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象static CGarbo Garbo;private:InfoMgr(){}~InfoMgr(){//假设析构时需要信息写到文件持久化,目的是单利对象销毁时要做一些必不可少的动作}InfoMgr(const InfoMgr&) = delete;string _address;int _secretKey;static InfoMgr* _spInst; //声明
};//先初始化成空
InfoMgr* InfoMgr::_spInst = nullptr; //定义
InfoMgr::CGarbo Garbo;int main()
{//全局只有一个InfoMgr对象InfoMgr::GetInstance()->SetAddress("122333");cout << InfoMgr::GetInstance()->GetAddress() << endl;return 0;
}

域饿汉模式不同的是,懒汉是模式则是一开始不创建对象,而是一开始先给一个指针,这样就避免了程序启动起来很慢的问题。

思路:

  • 将构造函数私有化
  • 一开始不构建对象,只是给一个空指针
  • 在需要的时候再调用其构造函数构造函数

补充:

  • 懒汉是一种延迟加载,饿汉是一开始就加载
  • 单例对象是new出来的,但是一般情况下单利对象不需要释放
  • 我们用的是虚拟内存,虚拟内存和物理内存之间还要建立一个映射
  • 在进程结束以后,内存也是会被还给操作系统的

虽然最终申请的单例对象还是会还给操作系统,但是我们还是手动释放一下更好~

所以定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象:
在这里插入图片描述