C++深入详解单例模式与特殊类设计的实现
作者:你算哪一个bug?
单例模式
什么是单例模式
单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。–大话设计模式
应用场景
保证一个类只有一个实例
- 如Windows下的任务管理器,回收站等。
- 日志管理,计数器等。
简而言之,你需要唯一实例时就可以考虑单例模式。这样它可以严格地控制客户怎样访问即何时访问它,即对唯一实例的受控访问。
优缺点
优点
- 减少内存开销,因为在系统中只有一个实例。
- 避免了频繁的创建和销毁对象,提高了性能
- 避免对资源的多重占用,比如单例时多人只写一个日志文件,如果有多个日志文件可能导致对相同的日志文件进行写操作
- 设置全局访问点
缺点
- 职责过重,与单一职责存在冲突
- 不能继承(构造方法私有)
实现
单例模式有两种实现模式,懒汉模式和饿汉模式。
注意单例模式的概念,大概就是唯一实例且有全局访问点,我们从这两点入手。
唯一实例:构造函数私有+防拷贝
拷贝构造是一种构造方式,所以需要防止,构造函数私有不让别人new。
全局访问点:给一个公共的接口
饿汉模式
简单来说,把东西一开始就做好,需要的时候直接吃。
#include <iostream> using namespace std; class Singleton { public: static Singleton& GetInstance() { return _instance; } int GetRandom()//由于侧重点不是随机数 所以直接返回一个30 { return 30; } private: Singleton() {}//构造函数私有 Singleton(Singleton&) = delete;//防拷贝,被=delete修饰表明这个函数被删除,即可以只声明不实现,换言之禁用了该函数 Singleton& operator=(const Singleton&) = delete;//防拷贝 static Singleton _instance; }; Singleton Singleton::_instance;//类外必须初始化,类内只是声明 int main() { //1.调用 cout<<Singleton::GetInstance().GetRandom()<<endl; //2. Singleton& s = Singleton::GetInstance(); cout<<s.GetRandom()<<endl; return 0; }
通过防拷贝和构造函数私有化之后下面的几种办法都失效了
Singleton s;//err Singleton s1(s);//err Singleton s1=s;//err
从上面我们可以看出饿汉模式的优缺点了,有点显而易见就是实现很简单粗暴,缺点很明显,类加载时单例对象就已经生成了,即还没有用就已经加载出来了,比如这个资源很大,又在游戏启动时加载,那就会造成游戏启动很慢。且如果有多个单例对象启动时实例化顺序不确定(不同源文件类内的单例对象实例化顺序是不确定的,懒汉模式解决了这个问题,因为懒汉模式的实例化在函数内部,可以通过调用函数的顺序来解决实例化的顺序问题)。
类加载时静态初始化解决了线程安全问题。
懒汉模式
什么时候需要就什么时候做饭,然后吃。
class Singleton { public: static Singleton* GetInstance() { if (_instance == nullptr) { _mtx.lock();//double lock保证线程安全 if (_instance == nullptr)//必须再次检查 不然可能另一个线程那已经new完了,这边又new违背了单例,且可能覆盖数据。 { _instance = new Singleton(); } _mtx.unlock(); } return _instance; } int GetRandom() { return 30; } class Clear//资源回收的内部类,必须是公有的,不然外部声明报错 { public: ~Clear() { cout << "释放资源" << endl; delete _instance; } }; private: static Clear _cle; Singleton() {} Singleton(Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; static Singleton* _instance; static mutex _mtx;//线程安全 }; Singleton* Singleton::_instance = nullptr; mutex Singleton::_mtx; Singleton::Clear _cle; int main() { //1. cout<<Singleton::GetInstance()->GetRandom()<<endl; //2/ static Singleton* s = Singleton::GetInstance(); cout << s->GetRandom() << endl; //不能用左值引用接收的原因 返回值是右值 得用右值引用接收 //为什么函数返回值是右值 因为返回值是借助临时变量返回的拿不到地址 //如果返回值是左值引用就能用左值引用接收 return 0; }
懒汉模式的优缺点也很明显,优点是什么时候第一次用就什么时候实例化,此外可以通过调用函数解决多个单例对象实例化的顺序问题,缺点就是写起来复杂,要考虑线程安全和内存泄露方面的问题。懒汉采用的是指针也更好回收资源(饿汉采用的是对象)
懒汉可能因为多线程丢数据,线程加锁保证在多线程环境下一定只有一个线程去new对象,只创建出一个单例对象,加锁可能导致频繁切换上下文,double lock解决。
特殊类设计
我们一般会通过构造函数,拷贝构造和赋值重载创建对象,现在把这几种方法全禁用了,然后自己再写一个外部可调用的接口来自定义类的创建方式。
当然也有别的办法,这种经常可以作为一种通解思路。
下面的禁用采用C++11的delete关键字实现,作用是禁止编译器生成默认的函数版本,即有声明但无实现。
设计一个类只能在堆上创建对象
简单来说只能通过new来创建对象。
方法一
构造方法禁用,然后给一个接口让外部调用。
构造方法的禁用=构造函数私有+禁用拷贝构造+禁用赋值重载
class HeapOnly { public: static HeapOnly* GetInstance() { return new HeapOnly; } void Test() { cout << "I am Test" << endl; } private: HeapOnly() {} HeapOnly(HeapOnly&) = delete; HeapOnly& operator=(const HeapOnly&) = delete; }; int main() { HeapOnly* ho = HeapOnly::GetInstance(); ho->Test(); delete ho; return 0; }
方法二
析构函数私有
对象建立在栈上,是由编译器分配空间的,编译器管理对象的生命周期,对象使用完后编译器会检查这个对象所有的非静态函数,包括析构函数,当编译器发现析构函数不能访问后就不能回收这块空间,所以编译器无法为其分配空间,编译器检查到这种情况也会报错。
析构函数私有的方法不建议使用,因为在类外无法使用delete释放空间,容易造成内存泄漏。
class HeapOnly { public: void Test() { cout << "I am Test" << endl; } private: ~HeapOnly(); }; int main() { //HeapOnly ho_stack;//err HeapOnly* ho = new HeapOnly; ho->Test(); return 0; }
只能在栈上创建对象
方法一
不能使用new --> 重载operator new即可。
这种存在一个缺陷,就是仍然可以在静态区创建对象
方法二
将构造函数设为私有再自定义一个接口
这里不用禁用构造函数,拷贝构造,赋值重载,因为对于下面的场景来说,匿名对象的拷贝构造更符合场景,编译器会选拷贝构造来进行构造,所以如果我们禁用了拷贝构造会报错,因为我们的delete关键字是有声明无实现,并不是真的把这个函数连带声明删除了。所以StackOnly()一看有我们自己写的拷贝构造的声明就会去匹配这个拷贝构造,我们自己写的拷贝构造又没有实现拷贝功能就会报错。
理解这个和编译链接知识有关,编译器看到声明就去匹配了,而不是一定要看到函数实现才匹配,链接时候才会去找实现,当发现有声明无实现时就很容易导致链接错误。
一个类不能被继承
父类构造函数私有即可,构造时先构造父类再构造子类,父类构造不出不能继承。
最后
关于单例模式,可以说是只能创建一个对象的实现。
还有个小问题,为什么不用全局变量代替单例模式,全局定义一个唯一的变量不就行了,合理吧[doge],理论上可以,但是非常不建议,可以自行查找资料"为什么不建议使用全局变量"。
全局变量的使用可能带来很多问题,而且很容易造成链接、重定义等错误,如果多人协作时有人再给这个变量起个别名,那维护代码时代价就太大了,这还只是全局变量的一个小缺点。
牛客代码规范评分里也不建议用全局变量,当然写题时代码没那么长,几个全局变量倒时不打紧。
.h不能包含定义,不然多个cpp去包含就会有链接错误,尽量把定义和声明分离开来
【82】【Cherno C++】【中字】C++的单例模式_哔哩哔哩_bilibili
到此这篇关于C++深入详解单例模式与特殊类设计的实现的文章就介绍到这了,更多相关C++单例模式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!