C++ 特殊类的设计

发布于:2024-12-22 ⋅ 阅读:(14) ⋅ 点赞:(0)

前言

在有些开发场景下需要我们设计出一些特殊的类来满足特殊的需求,本期我们将来介绍一下常见的特殊类的设计!

目录

前言

一、设计一个类,不能被拷贝

二、设计一个类,只能在堆上创建对象

三、设计一个类,只能在栈上创建对象

四、设计一个类,不能被继承

五、设计一个类,只能创建一个对象(单例模式)

1、饿汉模式

2、懒汉模式


一、设计一个类,不能被拷贝

拷贝只会在两个场景中:拷贝构造 和 赋值运算符重载,因此想要让一类禁止拷贝,只需要让该类不能调用 拷贝构造运算符重载 即可!

C++98

将一个类的拷贝构造和赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。

class CopyBan
{
public:
	CopyBan()
	{}

private:
	// C++98 将 拷贝构造 和 赋值运算符重载 声明 为私有
	CopyBan(const CopyBan&);
	CopyBan& operator=(const CopyBan&);
};

1、为什么要将声明设置为私有?

如果将 拷贝构造 和 赋值拷贝 的声明被设置成私有,用户有可能在类外面自己实现,这样就达不到实现禁止拷贝的目的了!

2、为什么只是声明而不实现?

不实现是因为他两根本是不会被调用的,及时定义了也没啥意义。所以,不实现更简单!

C++11 :

C++11 扩展了 delete 的用法,delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上 =delete 表示让编译器删除该默认成员函数!

class CopyBan
{
public:
	CopyBan()
	{}

	// C++11 将 拷贝构造 和 赋值运算符重载 让编译器删除掉
	CopyBan(const CopyBan&) =delete;
	CopyBan& operator=(const CopyBan&) = delete;
};

二、设计一个类,只能在堆上创建对象

方式一:

1、将类的构造函数私有化拷贝够声明为私有/禁用。防止别人调用拷贝构造在栈上生成对象!

2、提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建!

class HeapOnly
{
public:
	// 提供一个静态的 创建堆对象的成员函数
	static HeapOnly* CreateObject()
	{
		return new HeapOnly;
	}

private:
	HeapOnly()
	{}

	HeapOnly(const HeapOnly&); // C++98 将拷贝构造的声明私有化,防止利用拷贝构造在栈上创建对象
	
	// C++11 禁用掉拷贝构造
	// HeapOnly(const HeapOnly&) = delete;
};

方式二:

将析构函数私有化。将析构函数私有化之后,不能直接创建对象了。只能使用 new 来创建对象即在上。现在的问题是:new 完之后如何释放呢?因为析构被设置成了私有,所以不能直接调,此时需要提供一个成员函数,在这成员函数内部直接  delete this 即可,谁调用 this 就是谁,更好解决了析构私有化不能调用的问题!

class HeapOnly
{
public:
	void Destroy()
	{
		delete this;
	}

private:
	~HeapOnly()
	{
		cout << "~HeapOnly()" << endl;
	}
};

首先看一下直接创建对象不成功:

只能使用 new 创建

三、设计一个类,只能在栈上创建对象

和上面的思路类似。将构造函数私有化,然后设计一个静态的方法创建对象返回即可!

class StackOnly
{
public:
	static StackOnly CreateObject()
	{
		return StackOnly();
	}

private:
	StackOnly(){}
};

由于没有将 new 处理所以有时候下面这种情况还是可以通过的:

int main()
{
	StackOnly st1 = StackOnly::CreateObject();
	StackOnly* st2 = new StackOnly(st1);// st2 就是堆上的对象

	return 0;
}

这里你可能会想:直接把拷贝构造直接禁用掉不就完全解决了嘛?表面上看好像没问题,但实际上是不行的!因为 CreateObject() 函数返回的时临时对象,所以必须是传值返回,所以不能将拷贝构造删除。

这里的解决方法是:在我们自己的类中重载 operator new 然后将他禁用掉就OK了!为什么将我们重载的 禁用掉用好了呢?原因是,我们平时的 new 是全局的,当我们自己类中实现之后默认就是用的是当前类的,这里禁用掉之后就无法 new

class StackOnly
{
public:
	static StackOnly CreateObject()
	{
		return StackOnly();
	}
	
	// 实现当前类的 专属 new
	void* operator new(size_t) = delete;

private:
	StackOnly(){}
};

四、设计一个类,不能被继承

C++98:将构造函数私有化,派生类中调用不到基类的构造函数,即无法继承

class NonTherit
{
public:
	static NonTherit GetInstance()
	{
		return NonTherit();
	}

private:
	NonTherit()
	{}
};

C++11 :使用 final 关键字,final 修饰的类,表示该类不能被继承

class NonInherit final
{
public:
	NonInherit()
	{}
private:
	// ...
};

五、设计一个类,只能创建一个对象(单例模式)

设计模式:设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结/套路。

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

单例模式一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式很常用,例如我们前面实现的 线程池 就是使用的 单例模式

单例模式的两种实现:饿汉模式懒汉模式

1、饿汉模式

不管你未来用不用,在程序启动时就创建一个唯一的实例对象。

首先既然是唯一的一个实例,那必然不能拷贝,所以我么必须得把 拷贝构造 和 赋值拷贝 给删除掉。然后将 构造函数 私有化!在向外提供一个 static 的 获取该类对象的方法!

如何保障在进程启动即执行 main 时 就已将有一个创建好的对象呢?我们可以声明一个的静态成员变量(不属于类,本质是全局的),然后在类外面定义;这样就可以保证在进程启动时就已有唯一的对象了

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		return &m_instance;
	}
private:
	// 私有化构造
	Singleton() {} 
	// 禁用掉拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
	// 创建一个 static Singleton 的成员属性
	static Singleton m_instance;
};

Singleton Singleton::m_instance;// 再类外定义

我们如何验证此时的对象只有一个呢?我们可以打印获取到单例对象的地址:

• 饿汉模式的优点:实现简单 

• 饿汉模式的缺点:可能会导致进程启动慢;如果两个单例有启动先后顺序,那么饿汉无法控制

如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。因为在程序启动前需要进行初始化,如果需要初始化的资源很多,就会降低程序的启动速度。

2、懒汉模式

如果单例对象构造十分耗时或者占用很多资源,比如加载插件, 初始化网络连接,读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。懒汉模式就是在我们需要使用时,第一次才给我们创建对象

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 存在线程安全的问题--》加锁
		unique_lock<mutex> lock(_mtx);
		if (m_instance == nullptr)
		{
			m_instance = new Singleton();
		}

		return m_instance;
	}
private:
	// 私有化构造
	Singleton() {}
	// 禁用掉拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
	// 声明一个 static Singleton 的成员属性
	static Singleton* m_instance;
	// 保证线程安全,声明一把互斥锁
	static mutex _mtx;
};

Singleton* Singleton::m_instance = nullptr;// 再类外定义
mutex Singleton::_mtx;// 再类外定义

这种情况下,还可以稍微优化一下:因为枷锁还是要消耗时间的,如果是第一次还好不是第一次的话后面的每一次都要“傻傻的”加锁,在白白的消耗资源,所以我们可以在这里进行提前判断一下

static Singleton* GetInstance()
{
	// 双重判断
	if (m_instance == nullptr)
	{
		// 存在线程安全的问题--》加锁
		unique_lock<mutex> lock(_mtx);
		if (m_instance == nullptr)
		{
			m_instance = new Singleton();
		}
	}

	return m_instance;
}

• 懒汉模式的优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制

• 懒汉模式的缺点:复杂

上述的懒汉式实现的单例,有一点点小问题就是 new 的那个单例对象没有释放没可能会造成内存泄漏的问题!这里可以等进程结束的时候释放,也可以自己写一个回收机制,这里我们也实现一个简单的gc

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 双重判断
		if (m_instance == nullptr)
		{
			// 存在线程安全的问题--》加锁
			unique_lock<mutex> lock(_mtx);
			if (m_instance == nullptr)
			{
				m_instance = new Singleton();
			}
		}

		return m_instance;
	}

	// 回收资源的 GC
	class GC
	{
	public:
		~GC()
		{
			if (Singleton::m_instance)
				delete Singleton::m_instance;
		}
	};

	static GC gc;// 定义一个静态成员变量,程序结束时,系统自动调用它的析构释放资源
private:
	// 私有化构造
	Singleton() {}
	// 禁用掉拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
	// 声明一个 static Singleton 的成员属性
	static Singleton* m_instance;
	// 保证线程安全,声明一把互斥锁
	static mutex _mtx;
};

Singleton* Singleton::m_instance = nullptr;// 再类外定义
mutex Singleton::_mtx;// 再类外定义

我们这次使用多线程测试一下:

int main()
{
	thread t1([] {cout << Singleton::GetInstance() << endl; });
	thread t2([] {cout << Singleton::GetInstance() << endl; });

	t1.join();
	t2.join();

	cout << Singleton::GetInstance() << endl;
	cout << Singleton::GetInstance() << endl;

	return 0;
}


OK,本期分享就到这里,我是 CP 我们下期再见~!