特殊类的设计
目录
一、不能被拷贝的类
二、只能在堆区上创建对象
三、只能在栈区上创建对象
四、不能被继承的类
五、单例模式(只能创建一个对象)
5.1 概念
5.2 饿汉模式
5.3 懒汉模式
5.4 饿汉与懒汉模式对比
六、单例对象的释放
一、不能被拷贝的类
要让一个类不能被拷贝,就要让该类不能调用拷贝构造函数和赋值运算符重载函数,直接将该类的拷贝构造函数和赋值运算符重载函数设置为私有,或者用C++11的方式将这两个函数删除即可
class Test
{
public:Test() {}//...
private://C++98Test(const Test&);Test& operator=(const Test&);//C++11//Test(const Test&) = delete;//Test& operator=(const Test&) = delete;
};
二、只能在堆区上创建对象
方法一
只能在堆上创建对象,即只能通过new操作符创建对象,方式如下:
- 将构造函数设置为私有,防止外部直接调用构造函数在栈上创建对象
- 向外部提供一个获取对象的static接口,该接口在堆上创建一个对象并返回
- 将拷贝构造函数只声明不实现,并设置为私有权限,防止外部调用在栈上创建对象
#include <iostream>
using namespace std;
class HeapOnly
{
public://提供公有的获取对象的静态方法static HeapOnly* CreateObj(){return new HeapOnly;}
private:HeapOnly() :_data(0) {};HeapOnly(const HeapOnly& tmp) = delete;
private:int _data;
};
int main()
{HeapOnly* obj = HeapOnly::CreateObj();delete obj;return 0;
}
方法二
可以将析构函数私有,无法通过析构函数释放对象资源,即只能被迫在堆区上创建对象,使用delete操作符进行对象资源的释放
#include <iostream>
using namespace std;
class HeapOnly
{
public:void Delete() { delete this; }
private:~HeapOnly() {}
private:int _data;
};
int main()
{HeapOnly* p = new HeapOnly;p->Delete();return 0;
}
三、只能在栈区上创建对象
- 将构造函数设置为私有,防止外部直接调用构造函数在堆区上创建对象
- 向外部提供一个获取对象的static接口,该接口在栈区上创建一个对象并返回
- 屏蔽operator new()函数和operator delete()函数(防止调用拷贝构造在堆区创建对象)
#include <iostream>
using namespace std;
class StackOnly
{
public:static StackOnly CreateObj() {return StackOnly();}void* operator new(size_t size) = delete;void operator delete(void* p) = delete;
private:StackOnly():_data(0) {};
private:int _data;
};
int main()
{StackOnly so = StackOnly::CreateObj();static StackOnly s1(so);//无法完全实现要求return 0;
}
但该方法有一个缺陷:无法防止外部调用拷贝构造函数在数据段创建对象
但是不能将构造函数设置为私有,也不能使用=delete的方式将拷贝构造函数删除,因为CreateObj()函数中创建的是局部对象,返回局部对象的过程中必然需要调用拷贝构造函数
四、不能被继承的类
方法一(C++98)
将该类的构造函数设置为私有即可,因为子类的构造函数被调用时,必须调用父类的构造函数初始化父类的那一部分成员,但父类的私有成员在子类当中是不可见的,所以在创建子类对象时子类无法调用父类的构造函数对父类的成员进行初始化,因此该类被继承后子类无法创建出对象
class Perent
{
public:static Perent CreatePerentObj() {return Perent();}
private:Perent() {};
};
方法二(C++11)
C++98的这种方式并不够彻底,因为这个类仍然可以被继承(编译器不会报错),只不过被继承后无法实例化出对象而已。于是C++11中提供了final关键字,被final修饰的类被称为最终类,最终类无法被继承,就算继承后没有创建对象也会编译出错
class NonInherit final
{//...
};
五、单例模式(只能创建一个对象)
5.1 概念
- 单例模式是一种设计模式(Design Pattern),设计模式就是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式的目的就是为了可重用代码、让代码更容易被他人理解、保证代码可靠性程序的重用性
- 单例模式指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享
- 如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理
- 单例模式有两种实现方式,分别是饿汉模式和懒汉模式
5.2 饿汉模式
- 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象
- 提供一个指向单例对象的static指针,并在程序入口之前完成单例对象的初始化
- 提供一个全局访问点获取单例对象
class SingleTon
{
public:static SingleTon* GetInstance() {//全局访问点return _inst;}
private:SingleTon() {};//构造函数私有//防拷贝SingleTon(const SingleTon&) = delete;SingleTon& operator=(const SingleTon&) = delete;static SingleTon* _inst;//指向单例对象的static指针
};
SingleTon* SingleTon::_inst = new SingleTon;//在程序入口前完成单例对象的初始化
线程安全相关问题:
- 饿汉模式在程序运行主函数之前就完成了单例对象的创建,由于main()函数之前是不存在多线程的,因此饿汉模式下单例对象的创建过程是线程安全的
- 后续所有多线程要访问这个单例对象,都需要通过调用GetInstance()函数来获取,这个获取过程是不需要加锁的,因为这是一个读操作
5.3 懒汉模式
- 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象
- 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空
- 提供一个全局访问点获取单例对象,并在第一次访问全局访问点时完成单例对象的初始化
#include <thread>
#include <mutex>
using namespace std;
class SingleTon
{
public:static SingleTon* GetInstance() {//全局访问点if (nullptr == _inst){unique_lock<mutex>(_mtx);if (nullptr == _inst) {_inst = new SingleTon;}}}
private:SingleTon() {};//构造函数私有//防拷贝SingleTon(const SingleTon&) = delete;SingleTon& operator=(const SingleTon&) = delete;static SingleTon* _inst;//指向单例对象的static指针static mutex _mtx;
};
SingleTon* SingleTon::_inst = nullptr;
mutex SingleTon::_mtx;//定义,在程序入口前完成初始化
线程安全相关问题:
- 懒汉模式在程序运行之前没有进行单例对象的创建,而是等到某个线程需要使用这个单例对象时再进行创建,也就是GetInstance()函数第一次被调用时创建单例对象
- 因此在调用GetInstance()函数获取单例对象时,需要先判断这个static指针是否为空,如果为空则说明这个单例对象还没有创建,此时需要先创建这个单例对象然后再将单例对象返回
- GetInstance()函数第一次调用时需要对static指针进行写入操作,这个过程不是线程安全的,因为多个线程可能同时调用GetInstance()函数,若不对这个过程进行保护,此时这多个线程就会各自创建出一个对象
双检查加锁:
- 对GetInstance()函数中创建单例对象的过程进行保护,本质需要引入互斥锁,最简单的加锁方式就是在进行if判断之前加锁,在整个if语句之后进行解锁
- 但实际只有GetInstance函数第一次被调用,创建单例对象时需要使用互斥锁进行保护,而后续调用GetInstance函数获取单例对象只是一个读操作,是不需要使用互斥锁进行保护的
- 若简单的将加锁解锁操作放到if语句前后,那么在后续调用GetInstance()函数获取已经创建好的单例对象时,就会进行大量无意义的加锁解锁操作,导致线程不断切入切出,进而影响程序运行效率
- 对于这种只有第一次需要加锁保护的场景可以使用双检查加锁,双检查就是在当前加锁和解锁的外面再进行一次if判断,判断static指针是否为空
- 后续调用GetInstance()函数获取已经创建好的单例对象时,外层新加的if判断就会起作用,这样就避免了后续无意义的加锁解锁操作
- 加锁时最好使用unique_lock,因为new操作符是有可能抛异常的,可能出现未解锁的情况
其他实现方式
- 在单例类的GetInstance(0函数中定义一个静态的单例对象并返回
- 由于实际只有第一次调用GetInstance()函数时才会定义这个静态的单例对象,这也保证了全局只有这一个唯一实例
- 这里单例对象的定义过程是线程安全的,因为现在的C++标准保证多线程初始化static变量不会发生数据竞争,可以视为原子操作
- 该方法属于懒汉模式,因为局部静态变量不是在程序运行主函数之前初始化的,而是在第一次调用GetInstance()函数时初始化的
这种版本的懒汉主要有如下缺点:
- 单例对象定义在静态区,因此太大的单例对象不适合使用这种方式
- 单例对象创建在静态区后没办法主动释放
- 只有在C++11版本及以后可以这种方式
class SignleTon
{
public:static SignleTon* GetInstance() {static SignleTon inst;return &inst;}
private:SignleTon() {}SignleTon(const SignleTon&) = delete;SignleTon& operator=(const SignleTon&) = delete;
};
5.4 饿汉与懒汉模式对比
- 饿汉模式的优点是简单,但其缺点也比较明显。饿汉模式在程序运行主函数之前就会创建单例对象,若单例类的构造函数中所做的工作较多,就会导致程序迟迟无法进入主函数,在外部看来就好像是程序卡住了
- 若有多个单例类需要创建单例对象,并且它们之间的初始化存在某种依赖关系,如单例对象A的创建必须在单例对象B之后,此时饿汉模式也会存在问题,因为无法保证多个单例对象中的哪个对象先创建。
- 而懒汉模式就能很好的解决上述饿汉模式的缺点,因为懒汉模式并不是一开始就完成单例对象的创建,因此不会导致程序迟迟无法进入主函数,并且懒汉模式中各个单例对象创建的顺序是由各个单例类中的GetInstance()函数第一次被调用的顺序决定,因此是可控制的
- 懒汉模式的缺点就是,在编码上比饿汉模式复杂,在创建单例对象时需要考虑线程安全的问题
六、单例对象的释放
单例对象创建后一般在整个程序运行期间都可能会使用,所以可以不考虑单例对象的释放,程序正常结束时会自动将资源归还给操作系统
若要考虑单例对象的释放,可以参考以下两种方式:
1. 在单例类中编写一个DelInstance()函数,在该函数中进行单例对象的释放动作,当不再需要该单例对象时就可以主动调用DelInstance()释放单例对象
static void DelInstance()
{unique_lock<mutex>(_mtx);if (_inst != nullptr){delete _inst;_inst = nullptr;}
}
2. 在单例类中实现一个内嵌的垃圾回收类,在垃圾回收类的析构函数中完成单例对象的释放。在单例类中定义一个静态的垃圾回收类对象,当该对象被回收时就会调用其析构函数,这时便对单例对象进行了释放
class CGarbo//垃圾回收类
{
public:~CGarbo(){if (_inst != nullptr) {delete _inst;_inst = nullptr;}}
};