本篇博客让我们来康康一些特殊类的实现方式!
1.不支持拷贝的类 在一些场景下,比如智能指针、多线程操作、IO流等是不支持拷贝的。因为它们的拷贝会导致一些问题,秉着解决不了问题,就解决提出问题的人
的思路,禁止了这些类的拷贝
C++98
中,可以将拷贝构造和=重载
只声明不定义,并将其访问权限设置为私有
C++11
中,提供了一个特殊的关键字delete
来禁止实现拷贝构造和 =重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class BanCopy { public : BanCopy () { _a = _b = 0 ; } BanCopy (const BanCopy& c) = delete ; BanCopy& operator =(const BanCopy& c) = delete ; private : int _a; int _b; };
2.只能在堆上创建的类 操作方法和上面的思路类似,只需要把构造函数私有化就可以了
同时还需要取消拷贝构造,否则可以用拷贝构造在栈上开一个新的对象 赋值重载不一定需要取消,因为赋值重载无法创建新对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class HeapOnly { public : static HeapOnly* CreatObj (int a,int b) { return new HeapOnly (a, b); } private : HeapOnly () :_a(0 ), _b(0 ) {} HeapOnly (int a,int b) :_a(a), _b(b) {} HeapOnly (const HeapOnly& h) = delete ; int _a; int _b; };
这样写了之后,想创建对象就可以调用static
函数来操作
而且因为我们并没有私有化析构函数,所以析构是可以正常调用的!
2.1 另类操作 还可以使用static
函数提供一个接口来专门处理析构,再把析构函数设计成私有 ,构造函数公有;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class HeapOnly { public : static HeapOnly* CreatObj (int a,int b) { return new HeapOnly (a, b); } static void DelObj (HeapOnly* ptr) { delete ptr; } HeapOnly () :_a(0 ), _b(0 ) {} HeapOnly (int a, int b) :_a(a), _b(b) {} private : HeapOnly (const HeapOnly& h) = delete ; ~HeapOnly () { _a = _b = 0 ; } int _a; int _b; };
这样设计了之后,直接在栈上/全局区 开辟空间会报错,但是new不受影响。
在栈上创建一个对象,编译此代码会报错,因为析构函数无法被正常访问,所以无法编译成功;
1 2 3 4 5 6 7 8 $ g++ test.cpp -o test test.cpp: In function ‘int main()’: test.cpp:133:14: error: ‘HeapOnly::~HeapOnly()’ is private within this context HeapOnly h3; ^~ test.cpp:122:2: note: declared private here ~HeapOnly() ^
因为析构私有了,所以delete
不能正确调用析构函数,我们需要使用static
函数指定指针进行析构
除了static函数的这种办法,还有另外一个法子可以不传入对象的指针;
1 2 3 4 5 void DelObj () { delete this ; }
直接用对象调用此成员函数即可
1 2 HeapOnly* h6 = new HeapOnly (); h6->DelObj ();
只不过这样操作可能有些不太好理解,视具体情况而定喽!
3.只能在栈上创建的类 相同的思路,设计一个static
的创建对象函数,来创建一个栈上的对象return
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class StackOnly { public : static StackOnly CreatObj () { return StackOnly (); } private : StackOnly () { _a = _b = 0 ; } int _a; int _b; };
这里我们必须要有拷贝构造,因为return
的时候,编译器如果不优化,那就是构造+拷贝,优化了之后才能变成直接构造
这是取决于平台的,如果禁用了拷贝,万一有些平台编译器没有做这种优化,你的代码就跑不动了
另外,还有一个方法便是禁用掉operator new()
,以此禁止了在堆上创建空间。如果用这种办法,构造函数就不需要设计为私有了 但是这两个办法都有个缺陷,那就是用户可以用拷贝构造 在静态区上创建一个对象。这只能算个小瑕疵,可以不用管它
4.单例模式 单例模式是设计模式 的其中一种
设计模式是一套被反复使用且较为流行的代码设计经验总结。
设计模式有非常多,感兴趣的老哥可以去搜专门的博客了解一下
单例模式:一个类只能创建一个对象。该模式可以保证在一个进程中,某一个类只会有一个实例化的对象
举个例子,比如服务器的配置信息是一个类,这个类就可以设计成单例模式,保证所有人访问到的配置信息完全相同,修改的时候也能同步给所有人。
4.1 饿汉 饿汉模式采用static
成员来实现单例,思路和上面也是一样的,让构造函数私有而无法创建其他对象
先来看看下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Singleton { public : static Singleton* GetInstance () { return _sgp; } void Print () { cout << "----- System Info -----" << endl; cout << " CPU " << _cpu << endl; cout << " GPU " << _gpu << endl; cout << " MEM " << _mem << endl; cout << "----- End -----" << endl; } private : Singleton () :_cpu("i9-12900ks" ), _gpu("RTX 4090" ), _mem("128GB" ) {} Singleton (const Singleton& s) = delete ; Singleton& operator ==(const Singleton& s) = delete ; string _cpu; string _mem; string _gpu; static Singleton* _sgp; }; Singleton* Singleton::_sgp = new Singleton ();
因为_sg/_sgp
这两个成员都在类内部声明的,所以它们属于整个类域,可以成功访问到内部的构造函数。
而在其他地方的对象由于没有办法访问到构造函数,而无法创建
由于饿汉模式是static对象,其初始化是在main函数之前进行的。如果采用饿汉模式的单例过多,程序迟迟没有运行到main
处,会导致一个程序启动很慢
4.2 懒汉 一开始不创建对象,第一调用GetInstance再创建对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class InfoMgr { public : static InfoMgr* GetInstance () { if (_sp == nullptr ) { _sp = new InfoMgr; } return _sp; } void SetAddress (const string& s) { _address = s; } string& GetAddress () { return _address; } private : InfoMgr () :_address("bilibili" ), _secretKey(1234 ) {} InfoMgr (const InfoMgr&) = delete ; InfoMgr& operator ==(const InfoMgr&) = delete ; string _address; int _secretKey; static InfoMgr* _sp; }; InfoMgr* InfoMgr::_sp = nullptr ;
这里我们将内部的_sp
定义为了nullptr
,如果谁第一个调用,做一个判断,如果是nullptr
就创建实例
由于懒汉可能会出现多个线程同时第一次访问这个单例,就会导致在两个线程中都在初始化这个单例,而某一次初始化会失败。这是一个线程安全问题,需要我们对单例进行加锁操作
多线程加锁问题,参考linux下的操作:C++线程操作 ; C++的操作以这个思路,修改为使用C++的thread库即可
4.3 二者优缺点 饿汉的优点
简单易用 因为是在main函数前初始化,处于单线程状态,没有线程安全问题 缺点:
但是初始化顺序不确定,如果有其他类的依赖关系,可能会出现依赖项B在当前单例A后初始化,导致A无法完成初始化而程序boom 饿汉单例是在main函数之前创建的,拖慢程序启动速度 懒汉的优点
第一次调用的时候才初始化变量,提高程序启动速度 可以控制初始化顺序,按顺序来初始化,避免依赖关系问题 缺点:
基于这两个的优缺点,让我想出来一个不算办法的办法
如果想控制饿汉的初始化顺序,可以在main
一启动的时候,就调用一个初始化函数来初始化这些单例。这样依旧会拖慢进程启动的顺序,但解决了初始化顺序的问题!
实际上,一个单例究竟要不要在main之前就初始化需要看具体情况的!
4.4 单例释放资源 一般情况下,单例的类是不需要手动释放的,因为整个进程都需要使用这个单例
但如果我们的单例和一个文件挂钩,进程结束的时候,需要将单例里面的信息保存到文件里面,要怎么操作?
可以写一个垃圾回收类,在最后调用析构来回收资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 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 () :_address("bilibili" ), _secretKey(1234 ) {} ~InfoMgr () { } InfoMgr (const InfoMgr&) = delete ; string _address; int _secretKey; static InfoMgr* _spInst; }; InfoMgr* InfoMgr::_spInst = nullptr ; InfoMgr::CGarbo Garbo;
4.5 static单例 有人会采用下面的方式来实现懒汉的单例,其采用static对象,让编译器自动帮我们实现单例!
全局static变量会在main之前初始化 局部static变量会在第一次调用的时候初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Singleton { public : static Singleton* GetInstance () { static Singleton _s; return &_s; } private : Singleton (){}; Singleton (Singleton const &) = delete ; Singleton& operator =(Singleton const &) = delete ; };
但是!这个操作并不通用,其取决于编译器和平台的实现。特别是在C++11之前;
C++11之后,保证了局部静态变量初始化时的线程安全,我们便可以采用这种办法来实现单例。
C++11中局部static变量的线程安全问题
但是!一定要确认你的代码只在C++11的环境下运行!!
4.6 线程安全 在创建单例的时候,我们需要考虑到线程安全的问题,具体请参考linux博客中线程池单例类 对线程安全的处理。特别是其中进行了两次nullptr
的判断的原因
5.不能被继承的类 C++98中,只需要将构造函数私有,派生类无法调用基类构造函数,也就无法继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class A {public : static A GetInstance () { return A (); } private : A () { _a = 0 ; } int _a; };
而C++11中提供了一个关键字final
,用这个关键字修饰类,就无法被继承
结语 几个特殊类到这里就讲解结束辣,其中懒汉多线程加锁还留了一个坑,待后续我会回来更新补上的!
感谢你看到最后!