本篇博客让我们来康康一些特殊类的实现方式!
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++线程操作 ;
 
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,用这个关键字修饰类,就无法被继承
结语 
几个特殊类到这里就讲解结束辣,其中懒汉多线程加锁还留了一个坑,待后续我会回来更新补上的!
感谢你看到最后!