特殊类的设计(含单例模式)

文章目录


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

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

C++98:

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

cpp 复制代码
class CopyBan
{
    // ...
    
private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};

原因:

  1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就不能禁止拷贝了
  2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。

C++11:

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

cpp 复制代码
class CopyBan
{
    // ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

二、设计一个只能在堆上创建的类

方式一:

  1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
  2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。
cpp 复制代码
class HeapOnly {
public:
	static HeapOnly* CreatObject() { return new HeapOnly(); }
private:
	HeapOnly() {}
	HeapOnly(const HeapOnly&) = delete;
};

这种方式需要提供创建对象的接口,那么有没有其它方式呢?

方式二:

  1. 将析构函数设置为私有。
  2. 另外生成一个public权限函数来,释放对象。
cpp 复制代码
class HeapOnly {
public:
	HeapOnly() {}
private:
	~HeapOnly() {}
};

原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

三、设计一个只能在栈上创建的类

方法:将 new 和 delete 重载为私有。

在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。

将new操作设置为私有,那么第一阶段就无法完成,就不能够再堆上生成对象。

cpp 复制代码
class StackOnly {
private:
	int _a;
public:
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;

	StackOnly() :_a(0) {}

};

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

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

cpp 复制代码
class NonInherit
{
public:
 	static NonInherit GetInstance(){ return NonInherit();}
private:
 	NonInherit() {}
};

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

cpp 复制代码
class A  final
{
    // ....
};

五、单例模式

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个

访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置

信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再

通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

1.懒汉模式

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

懒汉模式version1 -> 问题1:线程安全 问题2:内存泄露

cpp 复制代码
class Singleton {
private:
	static Singleton* _instance;
	Singleton() { cout << "构造函数" << endl; }
	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton&) = delete;
public:
	static Singleton* getInstance()
	{
		if (_instance == nullptr)
			_instance = new Singleton();
		return _instance;
	}
	~Singleton() { cout << "析构函数" << endl; }

};
Singleton* Singleton::_instance = nullptr;

运行上面的代码我们发现资源没有释放:

针对资源释放的问题,我们可以通过实现一个内嵌的垃圾回收类,定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象:

cpp 复制代码
class Singleton {
public:
	static Singleton* getInstance()
               	{
		if (_instance == nullptr)
			_instance = new Singleton();
		return _instance;
	}
	~Singleton() { cout << "析构函数" << endl; }

public:
	// 实现一个内嵌垃圾回收类    
	class CGarbo {
	public:
		~CGarbo() { 
			if (Singleton::_instance != nullptr) { delete Singleton::_instance; }
		}
	};
	// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
	static CGarbo Garbo;

private:
	static Singleton* _instance;
	Singleton() { cout << "构造函数" << endl; }
	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton&) = delete;
};
Singleton* Singleton::_instance = nullptr;
Singleton::CGarbo Garbo;

懒汉模式version2 -> 不足:要求使用智能指针,锁有开销

我们不仅可以用内部类的思想来解决资源泄露的问题,还可以使用智能指针:

cpp 复制代码
class Singleton {
public:
	typedef shared_ptr<Singleton> Ptr;
	static Ptr getInstance()
	{
		if (_instance == nullptr)
		{
			std::lock_guard<std::mutex> _lck(_mtx);
			if (_instance == nullptr)
				_instance = shared_ptr<Singleton>(new Singleton());
		}
		return _instance;
	}
	~Singleton() { cout << "析构函数" << endl; }

private:
	static Ptr _instance;
	static std::mutex _mtx;
private:
	Singleton() { cout << "构造函数" << endl; }
	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton&) = delete;
};
Singleton::Ptr Singleton::_instance = nullptr;
std::mutex Singleton::_mtx;

shared_ptrmutex都是C++11的标准,以上这种方法的优点是:

  • 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
  • 加了锁,使用互斥量来达到线程安全。这里使用了两个 if 判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,提高效率。

不足之处在于:

  • 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。
  • 还有更加严重的问题,在某些平台(与编译器和指令集架构有关),双检锁会失效!

懒汉模式version3 -> Meyers's 的单例

C++11 规定了 local static 在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。在 C++11 标准下,《 Effective C++》提出了一种更优雅的单例模式实现,使用函数内的 local static 对象。这样,只有当第一次访问getInstance()方法时才创建实例。这种方法也被称为Meyers' Singleton

cpp 复制代码
class Singleton {
private:
	Singleton() { cout << "构造函数" << endl; }
	~Singleton() { cout << "析构函数" << endl; }
	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton&) = delete;
public:
	static Singleton& getInstance()
	{
		static Singleton _instance;
		return _instance;
	}
};

2.饿汉模式

单例实例在程序运行时被立即执行初始化,如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。:

cpp 复制代码
class Singleton
{
private:
	static Singleton _instance;
private:
	Singleton() { cout << "构造函数" << endl; }
	~Singleton() { cout << "析构函数" << endl; }
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
public:
	static Singleton& getInstance() { return _instance; }
};
Singleton Singleton::_instance;
相关推荐
方竞25 分钟前
Linux空口抓包方法
linux·空口抓包
stm 学习ing26 分钟前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
海岛日记1 小时前
centos一键卸载docker脚本
linux·docker·centos
AttackingLin2 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
Ysjt | 深3 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
ephemerals__3 小时前
【c++丨STL】list模拟实现(附源码)
开发语言·c++·list
Microsoft Word3 小时前
c++基础语法
开发语言·c++·算法
学Linux的语莫3 小时前
Ansible使用简介和基础使用
linux·运维·服务器·nginx·云计算·ansible
一只小小汤圆3 小时前
opencascade源码学习之BRepOffsetAPI包 -BRepOffsetAPI_DraftAngle
c++·学习·opencascade