本篇博客让我们来康康一些特殊类的实现方式!
1.不支持拷贝的类
在一些场景下,比如智能指针、多线程操作、IO流等是不支持拷贝的。因为它们的拷贝会导致一些问题,秉着解决不了问题,就解决提出问题的人
的思路,禁止了这些类的拷贝
C++98
中,可以将拷贝构造和=重载
只声明不定义,并将其访问权限设置为私有
- 设置为私有可以防止其他人在类外定义
C++11
中,提供了一个特殊的关键字delete
来禁止实现拷贝构造和 =重载
1 | // 禁止拷贝的类 |
2.只能在堆上创建的类
操作方法和上面的思路类似,只需要把构造函数私有化就可以了
- 同时还需要取消拷贝构造,否则可以用拷贝构造在栈上开一个新的对象
- 赋值重载不一定需要取消,因为赋值重载无法创建新对象
1 | // 只能在堆上开辟 |
这样写了之后,想创建对象就可以调用static
函数来操作
而且因为我们并没有私有化析构函数,所以析构是可以正常调用的!
2.1 另类操作
还可以使用static
函数提供一个接口来专门处理析构,再把析构函数设计成私有,构造函数公有
1 | // 只能在堆上开辟 |
这样设计了之后,直接在栈上/全局区开辟空间会报错,但是new不受影响。
因为析构私有了,所以delete
不能正确调用析构函数,我们需要使用static
函数指定指针进行析构
除了这种办法,还有另外一个法子可以不传入指针
1 | //删除自己 |
直接用对象调用此函数即可
1 | HeapOnly* h6 = new HeapOnly(); |
只不过这样可能有些不太好理解,视具体情况而定喽!
3.只能在栈上创建的类
相同的思路,设计一个static
的创建对象函数,来创建一个栈上的对象return
1 | // 只能在栈上开辟 |
这里我们必须要有拷贝构造,因为return
的时候,编译器如果不优化,那就是构造+拷贝,优化了之后才能变成直接构造
这是取决于平台的,如果禁用了拷贝,万一有些平台编译器没有做这种优化,你的代码就跑不动了
- 另外,还有一个方法便是禁用掉
operator new()
,以此禁止了在堆上创建空间。如果用这种办法,构造函数就不需要设计为私有了
但是这两个办法都有个缺陷,那就是用户可以用拷贝构造在静态区上创建一个对象。这只能算个小瑕疵,可以不用管它
4.单例模式
单例模式是设计模式的其中一种
设计模式是一套被反复使用且较为流行的代码设计经验总结。
设计模式有非常多,感兴趣的老哥可以去搜专门的博客了解一下
单例模式:一个类只能创建一个对象。该模式可以保证在一个进程中,某一个类只会有一个实例化的对象
举个例子,比如服务器的配置信息是一个类,这个类就可以设计成单例模式,保证所有人访问到的配置信息完全相同,修改的时候也能同步给所有人。
4.1 饿汉
饿汉模式采用static
成员来实现单例,思路和上面也是一样的,让构造函数私有而无法创建其他对象
- 那我们的static对象要怎么创建呢?
先来看看下面的代码
1 | // 单例模式(饿汉) |
因为_sg/_sgp
这两个成员都在类内部声明的,所以它们属于整个类域,可以成功访问到内部的构造函数。
而在其他地方的对象由于没有办法访问到构造函数,而无法创建
由于饿汉模式是static对象,其初始化是在main函数之前进行的。如果采用饿汉模式的单例过多,程序迟迟没有运行到main
处,会导致一个程序启动很慢
4.2 懒汉
一开始不创建对象,第一调用GetInstance再创建对象
1 | // 懒汉 |
这里我们将内部的_sp
定义为了nullptr
,如果谁第一个调用,做一个判断,如果是nullptr
就创建实例
由于懒汉可能会出现多个线程同时第一次访问这个单例,就会导致在两个线程中都在初始化这个单例,而某一次初始化会失败。这是一个线程安全问题,需要我们对单例进行加锁操作
多线程加锁问题,参考linux下的操作:C++线程操作;
C++的操作以这个思路,修改为使用C++的thread库即可
4.3 二者优缺点
饿汉的优点
- 简单易用
- 因为是在main函数前初始化,处于单线程状态,没有线程安全问题
缺点:
- 但是初始化顺序不确定,如果有其他类的依赖关系,可能会出现依赖项B在当前单例A后初始化,导致A无法完成初始化而程序boom
- 饿汉单例是在main函数之前创建的,拖慢程序启动速度
懒汉的优点
- 第一次调用的时候才初始化变量,提高程序启动速度
- 可以控制初始化顺序,按顺序来初始化,避免依赖关系问题
缺点:
- 第一次调用的时候,加载会慢一些
基于这两个的优缺点,让我想出来一个不算办法的办法
如果想控制饿汉的初始化顺序,可以在main
一启动的时候,就调用一个初始化函数来初始化这些单例。这样依旧会拖慢进程启动的顺序,但解决了初始化顺序的问题!
实际上,一个单例究竟要不要在main之前就初始化需要看具体情况的!
4.4 单例释放资源
一般情况下,单例的类是不需要手动释放的,因为整个进程都需要使用这个单例
但如果我们的单例和一个文件挂钩,进程结束的时候,需要将单例里面的信息保存到文件里面,要怎么操作?
可以写一个垃圾回收类,在最后调用析构来回收资源
1 | // 懒汉 -- 一开始不创建对象,第一调用GetInstance再创建对象 |
4.5 static单例
有人会采用下面的方式来实现懒汉的单例,其采用static对象,让编译器自动帮我们实现单例!
- 全局static变量会在main之前初始化
- 局部static变量会在第一次调用的时候初始化
1 | class Singleton |
但是!这个操作并不通用,其取决于编译器和平台的实现。特别是在C++11之前;
C++11之后,保证了局部静态变量初始化时的线程安全,我们便可以采用这种办法来实现单例。
但是!一定要确认你的代码只在C++11的环境下运行!!
5.不能被继承的类
C++98中,只需要将构造函数私有,派生类无法调用基类构造函数,也就无法继承
1 | // c++98,构造私有 |
而C++11中提供了一个关键字final
,用这个关键字修饰类,就无法被继承
1 | //C++11直接用关键字final |
结语
几个特殊类到这里就讲解结束辣,其中懒汉多线程加锁还留了一个坑,待后续我会回来更新补上的!
感谢你看到最后!
- 本文标题:【C++】特殊类设计 | 单例模式
- 创建时间:2022-10-19 16:13:46
- 本文链接:posts/3618449948/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!