• 【C++】几种特殊类(包含单例模式)


    本篇博客让我们来康康一些特殊类的实现方式!

    1.不支持拷贝的类

    在一些场景下,比如智能指针、多线程操作、IO流等是不支持拷贝的。因为它们的拷贝会导致一些问题,秉着解决不了问题,就解决提出问题的人的思路,禁止了这些类的拷贝

    C++98中,可以将拷贝构造=重载只声明不定义,并将其访问权限设置为私有

    • 设置为私有可以防止其他人在类外定义

    C++11中,提供了一个特殊的关键字delete来禁止实现拷贝构造和=重载

    // 禁止拷贝的类
    class BanCopy
    {
    public:
    	//构造
    	BanCopy()
    	{
    		_a = _b = 0;
    	}
    	
    	//C++11
    	BanCopy(const BanCopy& c) = delete;
    	BanCopy& operator=(const BanCopy& c) = delete;
    
    private:
    	//C++98的办法,声明为私有且不定义
    	//BanCopy(const BanCopy& c);
    	//BanCopy& operator=(const BanCopy& c);
    	
    	int _a;
    	int _b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    image-20221019160903815

    2.只能在堆上创建的类

    操作方法和上面的思路类似,只需要把构造函数私有化就可以了

    • 同时还需要取消拷贝构造,否则可以用拷贝构造在栈上开一个新的对象
    • 赋值重载不一定需要取消,因为赋值重载无法创建新对象
    // 只能在堆上开辟
    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;
    	// 赋值不一定需要delete,因为赋值不能创建新对象
    	// HeapOnly& operator=(const HeapOnly& h) = delete;
    
    	int _a;
    	int _b;
    };
    
    • 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

    这样写了之后,想创建对象就可以调用static函数来操作

    image-20221019091538726

    而且因为我们并没有私有化析构函数,所以析构是可以正常调用的!

    另类操作

    还可以使用static函数提供一个接口来专门处理析构,再把析构函数设计成私有,构造函数公有

    // 只能在堆上开辟
    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;
    	// 赋值不一定需要delete,因为赋值不能创建新对象
    	// HeapOnly& operator=(const HeapOnly& h) = delete;
    
    	~HeapOnly()
    	{
    		_a = _b = 0;
    	}
    
    	int _a;
    	int _b;
    };
    
    • 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

    这样设计了之后,直接在栈上/全局区开辟空间会报错,但是new不受影响。

    因为析构私有了,所以delete不能正确调用析构函数,我们需要使用static函数指定指针进行析构

    image-20221019150534487

    除了这种办法,还有另外一个法子可以不传入指针

    //删除自己
    void DelObj()
    {
        delete this;
    }	
    
    • 1
    • 2
    • 3
    • 4
    • 5

    直接用对象调用此函数即可

    HeapOnly* h6 = new HeapOnly();
    h6->DelObj();
    
    • 1
    • 2

    只不过这样可能有些不太好理解,视具体情况而定喽!


    3.只能在栈上创建的类

    相同的思路,设计一个static的创建对象函数,来创建一个栈上的对象return

    // 只能在栈上开辟
    class StackOnly
    {
    public:
    	static StackOnly CreatObj()
    	{
    		return StackOnly();//创建匿名对象返回,编译器直接优化为一个构造
    
    		//这么写的话,就不能禁止拷贝构造
    		//StackOnly st;
    		//return st;
    	}
    	// 不能禁用拷贝构造,因为return的时候可能会调用(编译器优化是取决于平台的)
    private:
    	StackOnly()	{
    		_a = _b = 0;
    	}
    
    	int _a;
    	int _b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这里我们必须要有拷贝构造,因为return的时候,编译器如果不优化,那就是构造+拷贝,优化了之后才能变成直接构造

    这是取决于平台的,如果禁用了拷贝,万一有些平台编译器没有做这种优化,你的代码就跑不动了

    • 另外,还有一个方法便是禁用掉operator new(),以此禁止了在堆上创建空间。如果用这种办法,构造函数就不需要设计为私有了

    但是这两个办法都有个缺陷,那就是用户可以用拷贝构造在静态区上创建一个对象。这只能算个小瑕疵,可以不用管它

    image-20221019092924850

    4.单例模式

    单例模式是设计模式的其中一种

    设计模式是一套被反复使用且较为流行的代码设计经验总结。

    设计模式有非常多,感兴趣的老哥可以去搜专门的博客了解一下

    单例模式:一个类只能创建一个对象。该模式可以保证在一个进程中,某一个类只会有一个实例化的对象

    举个例子,比如服务器的配置信息是一个类,这个类就可以设计成单例模式,保证所有人访问到的配置信息完全相同,修改的时候也能同步给所有人。

    4.1 饿汉

    饿汉模式采用static成员来实现单例,思路和上面也是一样的,让构造函数私有而无法创建其他对象

    • 那我们的static对象要怎么创建呢?

    先来看看下面的代码

    // 单例模式(饿汉)
    // 饿汉模式采用static对象,是在main函数之前创建的
    // 会影响程序启动的速度
    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;
    
    	string _cpu;
    	string _mem;
    	string _gpu;
    
    	//static Singleton _sg;//声明
    	static Singleton* _sgp;//声明
    };
    
    //Singleton Singleton::_sg;//定义
    Singleton* Singleton::_sgp = new Singleton();//定义
    //因为这里的sg和sgp都是属于类里面的成员,不受访问限定符的限制,才可以正常调用构造函数
    
    • 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

    因为_sg/_sgp这两个成员都在类内部声明的,所以它们属于整个类域,可以成功访问到内部的构造函数。

    而在其他地方的对象由于没有办法访问到构造函数,而无法创建

    image-20221019153141746

    由于饿汉模式是static对象,其初始化是在main函数之前进行的。如果采用饿汉模式的单例过多,程序迟迟没有运行到main处,会导致一个程序启动很慢


    4.2 懒汉(多线程加锁未解决)

    // 懒汉
    // 一开始不创建对象,第一调用GetInstance再创建对象
    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;
    
    	string _address;
    	int _secretKey;
    
    	static InfoMgr* _sp; // 声明
    };
    
    InfoMgr* InfoMgr::_sp = nullptr; // 定义
    
    • 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

    这里我们将内部的_sp定义为了nullptr,如果谁第一个调用,做一个判断,如果是nullptr就创建实例


    由于懒汉可能会出现多个线程同时第一次访问这个单例,就会导致在两个线程中都在初始化这个单例,而某一次初始化会失败。这是一个线程安全问题,需要我们对单例进行加锁操作

    由于我还没有学到多线程操作,所以留在后面来补上!

    4.3 二者优缺点

    饿汉的优点

    • 简单易用
    • 因为是在main函数前初始化,处于单线程状态,没有线程安全问题

    缺点:

    • 但是初始化顺序不确定,如果有其他类的依赖关系,可能会出现依赖项B在当前单例A后初始化,导致A无法完成初始化而程序boom
    • 饿汉单例是在main函数之前创建的,拖慢程序启动速度

    懒汉的优点

    • 第一次调用的时候才初始化变量,提高程序启动速度
    • 可以控制初始化顺序,按顺序来初始化,避免依赖关系问题

    缺点:

    • 第一次调用的时候,加载会慢一些

    基于这两个的优缺点,让我想出来一个不算办法的办法

    如果想控制饿汉的初始化顺序,可以在main一启动的时候,就调用一个初始化函数来初始化这些单例。这样依旧会拖慢进程启动的顺序,但解决了初始化顺序的问题!

    实际上,一个单例究竟要不要在main之前就初始化需要看具体情况的!

    4.4 单例释放资源

    一般情况下,单例的类是不需要手动释放的,因为整个进程都需要使用这个单例

    但如果我们的单例和一个文件挂钩,进程结束的时候,需要将单例里面的信息保存到文件里面,要怎么操作?

    可以写一个垃圾回收类,在最后调用析构来回收资源

    // 懒汉 -- 一开始不创建对象,第一调用GetInstance再创建对象
    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;//定义
    
    • 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

    5.不能被继承的类

    C++98中,只需要将构造函数私有,派生类无法调用基类构造函数,也就无法继承

    // c++98,构造私有
    class A {
    public:
    	static A GetInstance()
    	{
    		return A();
    	}
    
    private:
    	A()
    	{
    		_a = 0;
    	}
    	int _a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    而C++11中提供了一个关键字final,用这个关键字修饰类,就无法被继承

    //C++11直接用关键字final
    class B final
    {
    	//...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    结语

    几个特殊类到这里就讲解结束辣,其中懒汉多线程加锁还留了一个坑,待后续我会回来更新补上的!

    感谢你看到最后

    加油

  • 相关阅读:
    ORACLE XXX序列 goes below MINVALUE 无法实例化的处理办法
    springboot+vue.js+Elementui在线课程管理系统
    【云开发】在 React Native 中使用 AWS Textract 实现文本提取
    apt & apt-get命令
    VVICAPI接口解析,实现根据ID取商品详情
    (二十四)大数据实战——Flume数据流监控之Ganglia的安装与部署
    Element UI的介绍
    网络文化经营许可证
    centos8同步时间安装时间校准服务
    Redis 内存优化神技,小内存保存大数据
  • 原文地址:https://blog.csdn.net/muxuen/article/details/127410034