我们分析,当一个类只允许在堆上创建对象,原本,正常创建对象一定会调用构造函数,或者拷贝构造,要使用构造函数或者拷贝构造去创建对象(别人调用拷贝构造会在栈上生成对象),是不能保证只在堆上创建的,所以我们需要将构造函数与拷贝构造声明私有,因为无法通过构造函数与拷贝构造创建,所以我们需要引入一个静态成员函数,在此函数中创建对象
实现方式: 1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。 2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
class HeapOnly { public: static HeapOnly* GetObj()//设置个静态成员函数创建对象 { return new HeapOnly; } private: HeapOnly() {} // C++98防拷贝:声明成私有 //HeapOnly(const HeapOnly& ); public: // C++11 : 声明成delete HeapOnly(const HeapOnly&) = delete; }; int x7() { //HeapOnly hp; //HeapOnly* p = new HeapOnly; //HeapOnly* p = HeapOnly::GetObj(); std::shared_ptr<HeapOnly> sp1(HeapOnly::GetObj()); std::shared_ptr<HeapOnly> sp2(HeapOnly::GetObj()); //HeapOnly copy(*sp1);//调用拷贝构造构造函数 system("pause"); return 0; }
同样的,当我们了解了只能在堆上创建对象的方式之后,我们可以仿照这种方式,创建一个仅能在栈上创建对象的类
方法一:同上将构造函数私有化,然后设计静态方法创建对象返回即可。
class StackOnly { public: static StackOnly GetObj() { return StackOnly(); } private: StackOnly() {} };
这样操作,就可以保证对象通过调用函数创建在栈上了
还有一种方式,就是直接禁掉new 函数,就不会在堆上创建对象了,但是这也有一个问题,就是无法避免在静态区创建的对象
// 这种方案存在一定程序缺陷,无法阻止在数据段(静态区)创建对象 class StackOnly { public: void* operator new(size_t size) = delete; }; int x8() { StackOnly so; //StackOnly* p = new StackOnly; static StackOnly sso; return 0; }
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此 想要让一个类禁止拷贝,只需让该类 不能调用拷贝构造函数以及赋值运算符重载即可 。
这其实就是将拷贝构造私有就可以了,别人也就无法进行拷贝了
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; //... };
这个其实就是将构造函数私有,而子类若要想继承就必须要调父类的构造函数,所以无法继承
C++98 方式
class NonInherit { public: static NonInherit GetInstance() { return NonInherit(); } private: NonInherit() {} };
C++11 方法 fifinal 关键字, fifinal 修饰类,表示该类不能被继承
class A final { // .... };
其实我们之前已经学过一些设计模式
迭代器模式--基于面向对象的三大特性之一,封装设计出来的,用一个迭代器封装以后,不暴露容器结构的情况下,统一的方式访问修改容器中的数据
适配器模式 -- 体现的是一种复用
还有一些常见的设计模式如:工厂模式、装饰器模式、观察者模式、单例模式...
一个类只能在全局(进程中)只有一个实例对象,就是单例模式
什么场景下使用?比如一个进程中有一个内存池,进程中的多线程需要内存都要到这个内存池中取,那么这个内存池的类就可以设计单例模式。
我们先来演示一个最简单的单例模式
class Singleton { public: static Singleton* GetInstance() { if (_pinst == nullptr) { _pinst = new Singleton; } return _pinst; } Singleton(const Singleton& s) = delete; private: Singleton() {} static Singleton* _pinst; }; Singleton* Singleton::_pinst = nullptr;
我们可以看到,我们首先将构造函数声明私有,使其只能通过静态的get函数来创建对象,其次在get函数中设置,第一次调用就创建对象,之后创建的直接返回这个指针,此时不管我们调用多少回也都是这个指针了,这便完成了我们最简单的单例模式
但其实这个单例模式是有问题的,它存在线程安全的问题,当有两个线程同时去调用get函数时,可能会出现都检测出为空,然后都创建了一个对象,在随后的调用中又会将第一个创建的覆盖掉,此时会出现内存泄漏
我们的解决方案就是加锁
class Singleton { public: static Singleton* GetInstance() { _mtx.lock(); if (_pinst == nullptr) { _pinst = new Singleton; } _mtx.unlock(); return _pinst; } Singleton(const Singleton& s) = delete; private: Singleton() {} static Singleton* _pinst; static mutex _mtx;//声明静态锁 }; Singleton* Singleton::_pinst = nullptr; mutex Singleton::_mtx;//定义锁
但这样其实也并不是完美的,我们在new对象时是可能出现异常的,抛出异常导致无法解锁,所以我们还需对其进行修改
static Singleton* GetInstance() { //::Sleep(1000); 增加没加锁时出现线程不安全的条件(2个以上线程同时过了判断条件) // 双检查 if (_pinst == nullptr) { //_mtx.lock(); unique_lock<mutex> lock(_mtx); if (_pinst == nullptr) { _pinst = new Singleton; } //_mtx.unlock(); } // ... return _pinst; } static void DelInstance()//这个函数也可以不加,对象在生命周期结束也会自动释放 { //unique_lock<mutex> lock(_mtx); delete _pinst; _pinst = nullptr; } Singleton(const Singleton& s) = delete; private: Singleton() {} static Singleton* _pinst; static mutex _mtx; };
我们加上智能锁守卫,使其解锁一定会被执行,而我们在外部还加了一个判断,这就是为了双重保险,直接阻止多个线程进入锁中的情况,我们开始的两个进程,一个堵在了unique_lock,一个进去创建对象,当进行创建过后,我们就不需要加锁了,所以在给外层加个if,进行优化
这其实就是我们单例模式中的懒汉模式,当我们走到创建对象的时候,为空,我们才开始创建对象,第一次获取对象时,创建对象
懒汉模式 如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载 )更好。
下面我们来看另一个单例模式,饿汉模式
// 饿汉模式 一开始(main函数之前)就创建对象 class Singleton { public: static Singleton* GetInstance() { return &_inst; } Singleton(const Singleton&) = delete; private: Singleton() {} static Singleton _inst; }; Singleton Singleton::_inst; // static对象是在main函数之前创建的,这会只有主线程,所以不存在线程安全。
我们饿汉模式,指的是在一开始就创建了对象,所以不存在线程安全问题
1、懒汉模式需要考虑线程安全和释放的问题,实现相对更复杂,饿汉模式不存在以上问题,实现简单
2、懒汉是一种懒加载模式需要时在初始化创建对象,不会影响程序的启动。饿汉模式则相反,程序启动阶段就创建初始化实力对象,会导致程序启动慢,影响体验。
3、如果有多个单例类,假设有依赖关系(B依赖A),要求A单例先创建初始化,B单例再创建初始化,那么就不能饿汉,因为无法保证创建初始化顺序,这时用懒汉我们就可以手动控制。总结一下:实际中懒汉模式还是更实用一些