【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;
};

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;
};

这样写了之后,想创建对象就可以调用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;
};

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

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

image-20221019150534487

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

//删除自己
void DelObj()
{
    delete this;
}	

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

HeapOnly* h6 = new HeapOnly();
h6->DelObj();

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


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;
};

这里我们必须要有拷贝构造,因为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都是属于类里面的成员,不受访问限定符的限制,才可以正常调用构造函数

因为_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; // 定义

这里我们将内部的_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;//定义

5.不能被继承的类

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

// c++98,构造私有
class A {
public:
	static A GetInstance()
	{
		return A();
	}

private:
	A()
	{
		_a = 0;
	}
	int _a;
};

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

//C++11直接用关键字final
class B final
{
	//...
};

结语

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

感谢你看到最后

加油


版权声明:本文为muxuen原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。