C++ 特殊类设计以及单例模式

news2024/9/23 11:26:16

目录

1 不能被拷贝

2 只能在堆上创建对象

3 只能在栈上创建对象

4 禁止在堆上创建对象

5 不能被继承的类

6 单例类


特殊类就是一些有特殊需求的类。

1 不能被拷贝

要设计一个防拷贝的类,C++98之前我们只需要将拷贝构造以及拷贝赋值设为私有,同时只声明不实现,就能防止拷贝。

class A
{
public:
	A() {}
private:
	A(const A& );
	A& operator=(A&);
};

而C++11新增了关键字delete之后,我们就可以直接删除这两个成员函数来达到防拷贝的目的。

class A
{
public:
	A() {}
private:
	A(const A& ) = delete;
	A& operator=(A&) =delete;
};

2 只能在堆上创建对象

要设计这样的类我们必须把构造函数私有,防止用户自己去创建对象,然后提供一个接口专门用来给用户创建堆上的对象返回,用户只有这一种方法能够获得对象,相当于从源头上杜绝在栈上创建对象。

要注意的是,我们的这个返回堆上的对象的接口必须是公有且静态的。

class A
{
public:
	static A* getA()
	{
		return new A();
	}
private:
	A(){};
	int _a = 0;
};

但是这样写的话还有一个漏洞,就是拷贝构造和拷贝赋值没有禁止,不禁止的话用户可能会利用这个漏洞来拷贝构造出栈上的对象,或者使用拷贝赋值玩出栈上的对象。

所以还是必须禁止掉拷贝构造和拷贝赋值

class A
{
public:
	static A* getA()
	{
		return new A();
	}
private:
	A(const A&) = delete;
	A& operator=(const A&) = delete;
	A(){};
	int _a = 0;
};

其实还有一种方法:就是直接将析构函数私有,而不管构造函数

这时候如果是在栈上创建的对象,由于析构函数是私有的,所以无法析构,这时候会在编译时就报错。

那么与此同时,我们就需要提供一个接口destroy用来销毁堆上的对象,销毁的方法也很简单,我们可以在里面调用delete this 来析构和释放 ,也可以直接显式调用析构函数。 注意析构函数要显式调用的话必须显式用this来调用。 

class A
{
public:
	A() {}
	static A* getA()
	{
		return new A();
	}
	void destroy()
	{
		//this->~A();  //也可以显式调用析构函数
		delete this;
	}
private:
	A(const A&) = delete;
	A& operator=(const A&) = delete;
	~A() {}
private:
	int _a = 0;
};

3 只能在栈上创建对象

首先还是要把构造函数私有,那么new的时候编译器就调用不了构造函数了,也就无法在堆上创建对象。但是如何获得栈上的对象呢?提供一个接口,创建一个对象并且返回,因为我们外面要接受的话,肯定是要发生拷贝,所以拷贝构造我们必须实现,但是如此一来,我们使用new的时候就可以调用拷贝构造了,所以单纯把构造函数私有是没有达到目标。

所以我们好像必须将拷贝构造和拷贝赋值私有,但是这样一来我们怎么获取栈上的对象呢?那么就不获取了,直接通过函数的返回值来充当临时对象来调用内部的方法。

class A 
{
public:
	static A getA()
	{
		return A();
	}
	void func() { cout << "func" << endl; }

private:
	A(){}
	A(const A& a){}
	A& operator=(const A&){}
};

如果我们觉得每次都要调用getA函数才能调用类的方法麻烦,我们也可以直接用一个const左值引用来接收返回值拷贝出来的临时对象,被const 左值引用之后,这个临时对象的生命周期就延长了,我们可以把它当作栈的对象来用。

	A::getA().func();
	const A& ra = A::getA();
	ra.func();

同时,我们把拷贝构造和拷贝赋值私有之后,也防止了在静态区创建对象,因为他要创建对象也只能通过 getA 函数的返回值来构造,但是我们已经把构造和拷贝构造都死有了,所以他也没办法创建对象。

4 禁止在堆上创建对象

最简单的办法就是将 operator new 和operator delete 删除。

	void* operator new(size_t size) = delete;
	void operator delete(void*) = delete;

因为 new 对象的时候是调用 operator new 和构造函数来在堆上申请对象的,同时在delete的时候也是调用 析构函数 和 operator delete 来释放对象的,那么我们只需要吧这两个接口删除,就无法创建在堆上的对象了。

5 不能被继承的类

C++11之前,我们可以将 所有构造函数设为私有 ,因为子类的构造函数中必须显式调用父类的构造函数,如果父类的构造函数是私有的话,子类是访问不到的。

第二种方法,就是在类的声明后面加上修饰符 final ,表示这是一个最终类,不能被继承。

6 单例类

 单例就是该类只能有一个对象。同时这也涉及到了一个设计模式:单例模式

单例模式: 一个类只能创建一个对象,即单例模式,该模式可以保证系统中只存在该类的一个实例,并提供一个访问它的全局访问点,该实例被所有程序模板共享。

也就是全局只有一个对象,这个对象必须很容易就能访问到。

其实设计起来就跟我们上面设计的只能在栈上创建对象的类有点类型,只能通过类提供的静态的接口来获取对象,然后通过这个对象来调用成员方法。

单例模式有两种实现方式,饿汉模式和懒汉模式

1 饿汉模式

指的是不管当前或者未来用不用这个对象,在程序或者服务器启动的时候,都先把对象创建出来。

要保证这个类只有一个对象,我们可以用一个静态的对象来表示这个唯一对象。这个静态对象当然可以设置为公有的,但是公有的太过随便,不安全,最好还是设为私有然后提供一个接口来返回这个对象的指针,外部通过返回值来进行调用。

同时,为了保证单例,我们必须将构造函数设为私有,然后拷贝构造和拷贝赋值直接删除。

class A
{
public:
	//获取单例的方法
	static const A* GetSingle()
	{
		return &single;
	}
	//类的其他的成员函数 ...
	void func()
	{
		cout << "func" << endl;
	}

private:
	A() {};
	A(const A& a) = delete;
	A& operator=(const A& a)=delete;
	
private:
	//类的成员

	//类的唯一实例
	static A single;
};
//初始化
A A::single;

这里大家可能会有两个疑惑?

1 类里面怎么能包含类自身的对象,计算类大小的时候不会出有问题吗?

因为这是静态成员,是整个类所共享的,他不是存在对象中,所有静态成员的大小并不也会算在类的大小中。

2 为什么能够在类外调用构造函数初始化 single ?

这是因为我们指明了类域,这其实是在类域中调用构造函数进行初始化。

饿汉模式是程序启动的时候对象就创建了,也就是在main函数执行之前就有了,我们上面将其设置为了全局的对象(作用域是类域,但是生命周期是从该对象被定义到程序结束),该对象我们在全局就定义好了,而全局对象是在main函数开始之前就已经创建好了,所以符合饿汉的条件。

同时为什么我们上面的getsingle不传值返回而是要指针返回呢?

因为我们把拷贝构造删除了,而传值返回是需要调用拷贝构造来构造一个临时对象的。不过除了传指针返回,我们更推荐传引用返回,因为这个对象是一直存在的,我们在外面使用的时候也可以用一个引用接收返回值,接收之后就不用每次都调用这个函数了。

	//返回引用
	static const A& GetSingle()
	{
		return single;
	}
	A::GetSingle().func();
	const A& ra = A::GetSingle();
	ra.func();

饿汉模式的单例是线程安全的,因为对象在程序加载的时候就创建出来了,外界每一次调用返回的都是这一个对象。

饿汉模式的缺点:

1 如果 单例对象初始化时数据太多 ,会导致程序或者说服务器启动慢。 

比如说这个单例的创建还需要去网络中和数据库中拿数据来进行构造,那么就会导致启动速度很慢,因为不管怎么样,只有构造完这个对象之后才能进入main函数执行

2 如果多个单例类有初始化的依赖关系,饿汉模式无法控制顺序。

因为单例都是在main函数之前进行初始化,而如果有多个单例对象需要初始化的时候,当他们不在同一个文件中,我们是无法保证哪个单例对象先被创建的,我们无法控制他们初始化的顺序。那么就会导致有依赖关系的单例对象的初始化出现问题。

所以饿汉模式在有些场景下就很不合适,于是又提出了一种新的方式: 懒汉模式

懒汉模式的特点:在第一次获取对象调用的时候才初始化

class A
{
public:
	//获取单例的方法
	//返回指针
	static const A* GetSingle()
	{
		//第一次调用这个函数的时候才初始化单例对象
		if(single==nullptr)
		single = new A();

		return single;
	}
	//类的其他的成员函数 ...
	void func()const
	{
		cout << "func" << endl;
	}

private:
	A() {};
	A(const A& a) = delete;
	A& operator=(const A& a)=delete;
	
private:
	//类的成员

	//类的唯一实例
	static A* single;
};

A* A::single = nullptr;

饿汉模式的优点:

1 对象在main函数之后才创建,不会影响启动速度

2 可以主动控制多个单例对象的创建顺序

我们可以通过调用的顺序来控制创建的顺序。

但是创建对象的时候是有线程安全问题的,所以我么需要锁来保证只有一个线程能创建对象。

class A
{
public:
	//获取单例的方法
	//返回指针
	static const A* GetSingle()
	{
		//第一次调用这个函数的时候才初始化单例对象
		mtx.lock();
		if(single==nullptr)
		single = new A();
		mtx.unlock();

		return single;
	}
	//类的其他的成员函数 ...
	void func()const
	{
		cout << "func" << endl;
	}

private:
	A() {};
	A(const A& a) = delete;
	A& operator=(const A& a)=delete;
	
private:
	//类的成员

	//类的唯一实例
	static A* single;
	static mutex mtx;
};

A* A::single = nullptr;
mutex A::mtx;

但是这样一来,每个线程在进入判断之前都要加锁才能判断,那么效率就低了,我们可以用双重判断来提高效率。

	static const A* GetSingle()
	{
		//第一次调用这个函数的时候才初始化单例对象
		if (single == nullptr)
		{
			mtx.lock();
			if (single == nullptr)
				single = new A();
			mtx.unlock();
		}
		return single;
	}

第一个判断是为了判断是不是第一次调用,那么如果已经存在对象了,我们就不需要进去了,也就不需要加锁和解锁了。

而加锁之后的 if 是用来判断是否需要创建对象,因为多线程的场景下,这个if可能会被多个线程同时执行到。 但是我们加锁之后,就可以避免多个线程同时进入这个if,就能保证只会创建一次对象。

最后还有一个问题就是,new的时候是可能会出错抛异常的,那么我们就需要捕获异常,并完成解锁。

	static const A* GetSingle()
	{
		//第一次调用这个函数的时候才初始化单例对象
		if (single == nullptr)
		{
			try 
			{
				mtx.lock();
				if (single == nullptr)
					single = new A();
				mtx.unlock();
			}
			catch (...)
			{
				mtx.unlock();
			}
		}
		return single;
	}

但是这样写的话代码不够美观。我们可以搞成 RAII 风格的锁。

			try
			{
				lock_guard<mutex> lock(mtx);
				if (single == nullptr)
					single = new A();

			}
			catch (...) { throw; }

懒汉模式我们还可以完善一下他的析构,不过一般单例是不需要释放的,因为他的生命周期一般是从创建开始到进程结束的,而进程结束的时候会自动释放所有资源,所以一般是不需要我们主动去销毁这个单例的。

但是考虑在有的场景下需要提前手动释放这个对象,那么我们可以提供一个destroy接口来释放这个单例对象,那么与此同时就必须要提供析构函数。

懒汉模式还有一种写法:

class A
{
public:
	static A& getsingle()
	{
		static A a;
		return a;
	}
private:
};

就是返回一个局部静态对象。

如果静态对象是局部对象的话,那么会在第一次定义的时候创建和初始化,也就是会在main函数之后的第一次调用该函数时进初始化。

但是这种静态的局部对象会出现线程安全问题吗?

在C++11之前,这里是不能保证这个局部的静态对象的初始化四线程安全的,所以C++11之前我们不是用这种方式。但是C++11之后,可以保证局部静态对象的创建是安全的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2046851.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在HFSS中对曲线等结构进行分割(Split)

在HFSS中对曲线进行分割 我们往往需要把DXF等其他类型文件导入HFSS进行分析&#xff0c;但是有时需要对某一个曲线单独进行分割成两段修改。 如果是使用HFSS绘制的曲线&#xff0c;我们修改起来非常方便&#xff0c;修改参数即可。但是如果是导入的曲线&#xff0c;则需要使用…

代码随想录训练营 Day31打卡 贪心算法 part05 56. 合并区间 738. 单调递增的数字 968. 监控二叉树

代码随想录训练营 Day31打卡 贪心算法 part05 一、 力扣56. 合并区间 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需恰好覆盖输入中…

【JavaEE】JVM 内存区域划分,以及 Java 垃圾回收机制引用计数器,可达性分析等

目录 1. JVM执行流程 2. JVM运行时数据区 2.1 堆 2.2 Java虚拟机栈(线程私有) 2.3本地方法栈(线程私有) 2.4 程序计数器 2.5 元数据区 3. JVM的类加载机制 1) 加载 2) 验证 3) 准备 4) 解析 5) 初始化 双亲委派模型 4. java垃圾回收 4.1 死亡对象判断方法 a) …

超精细CG杰作:8K壁纸级官方艺术插画,展现极致美丽与细节的汉服女孩

极致精美的数字艺术杰作&#xff1a;8K壁纸级别的官方插画&#xff0c;展现超高清细节与和谐统一的美感&#xff0c;女孩的精致面容与眼神在光影下熠熠生辉&#xff0c;汉服主题下的超高分辨率作品&#xff0c;文件巨大&#xff0c;细节丰富&#xff0c;令人惊叹。 正向提示词…

内存泄漏之如何使用Visual Studio的调试工具跟踪内存泄漏?

使用Visual Studio的调试工具跟踪内存泄漏是一个系统性的过程&#xff0c;主要包括启用内存泄漏检测、运行程序、分析内存使用情况以及定位泄漏源等步骤。 Visual Studio提供了多种方式来检测内存泄漏&#xff0c;你可以根据自己的需求选择合适的方法。 注意&#xff1a;下面…

父页面选项式api,子页面组合式api,子页面如何获取父页面的方法以及传值到将子页面的值传给父页面

开发的项目中是vue3的项目&#xff0c;但是有些同事用vue2中的选项式api写法&#xff0c;有些同事使用的是vue3组合式api的写法&#xff0c;此时子页面需要获取父页面的方法把数据传入父页面的方法中 父页面&#xff1a; 在父页面中order-item组件中创建自定义方法navigation和…

Leetcode每日刷题之剑指offer 57.和为s的两个数字(C++)

1.题目解析 现在题目改名为LCR.查找总价值为目标值的两个商品&#xff0c;虽然题目改变但是核心并未变化&#xff0c;都是需要寻找出和为指定数字的两数 2.算法原理 我们由题目知道给出的数组是递增的&#xff0c;所以在数组的首尾固定两个指针&#xff0c;判断其和是否为指定数…

Ceph篇之利用shell脚本实现批量创建bucket桶

Ceph创建bucket桶 在 Ceph 中创建桶&#xff08;bucket&#xff09;需要使用 Ceph 对象网关&#xff08;RGW&#xff09;。 注&#xff1a;如果查看shell批量创建脚本请直接参见目录3 1. 利用radosgw-admin工具创建桶 确保 Ceph 集群和对象网关已正确配置 确保你的 Ceph 集群…

快速了解Vi 和 Vim 编辑器三种模式及快捷键使用

&#x1f600;前言 本篇博文是关于Vi 和 Vim 编辑器的三种模式及快捷键使用&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到大家&#xff0c;您的满意…

大数据产业链图谱_产业链全景图_大数据行业市场分析

数据作为新型生产要素&#xff0c;是数字化、网络化、智能化的基础&#xff0c;已快速融入生产、分配、流通、消费和社会服务管理等各环节&#xff0c;影响着千行百业&#xff0c;推动着我国数字经济的蓬勃发展。 大数据又称巨量数据、海量数据&#xff0c;是由数量巨大、结构…

C语言 | Leetcode C语言题解之第341题扁平化嵌套列表迭代器

题目&#xff1a; 题解&#xff1a; struct NestedIterator {int *vals;int size;int cur; };void dfs(struct NestedIterator *iter, struct NestedInteger **nestedList, int nestedListSize) {for (int i 0; i < nestedListSize; i) {if (NestedIntegerIsInteger(neste…

Sprache:轻量级C#解析器构建,可用于字符串验证等。

我们在开发中&#xff0c;经常需要对一些结构化文本进行解析&#xff0c;用于验证是否符合规则。我们一般会使用正则表达式&#xff0c;同时正则表达式也非常强大&#xff0c;但正则表达式在语法不便阅读与维护。 下面介绍一个简单、轻量级的库&#xff0c;方便我们在C#代码中…

React 学习——打包后,包体积可视化

1、安装插件 &#xff08; source-map-explorer &#xff09; npm i source-map-explorer 2、在配置文件package.json中加入 &#xff08; "analyze": "source-map-explorer build/static/js/*.js" &#xff09;&#xff0c;位置截图 "analyze&q…

Flask 线上高并发部署方案实现

目录 1、Flask默认多线程执行 2、使用gevent.pywsgi实现 3、是用uWSGI服务器实现 1、Flask默认多线程执行 前言&#xff1a;在Flask的较早版本中&#xff0c;默认并不支持多线程模式。然而&#xff0c;从Flask 0.9版本开始&#xff0c;引入了多线程模式的支持&#xff0c;并…

红酒与旅游攻略:旅行途中的风味之选

在旅行的道路上&#xff0c;我们总是渴望寻找那些能够触动心灵、留下深刻记忆的不同体验。而红酒&#xff0c;作为一种充满韵味和故事的饮品&#xff0c;无疑是旅行途中的风味之选。洒派红酒&#xff08;Bold & Generous&#xff09;&#xff0c;这款定制红酒&#xff0c;以…

基于xilinx IP的频域脉冲压缩算法的实现和matlab仿真

工具&#xff1a;matlabR2021b&#xff0c;vivado2018.3. 脉冲压缩的原理 脉冲压缩实际上就是对接收信号进行匹配滤波处理。根据发射的波形不同&#xff0c;脉冲压缩时选择不同的匹配滤波器系数。 数字脉冲压缩的实现方式有两种: 一是时域卷积法; 二是频域乘积法。依据傅里叶…

智能化包括自动化与非自动化

智能化通常指的是系统或设备具备智能功能&#xff0c;以提高其自主性和效率。智能化可以分为自动化与非自动化两大类&#xff0c;每一类都有其独特的特点和应用场景。 一、自动化 自动化指的是系统能够在无需人为干预的情况下完成任务或操作。自动化系统通常依赖于预设的规则、…

基于LangChain手工测试用例转接口自动化测试生成工具!

接口自动化测试用例是一个老生常谈的问题&#xff0c;在未引入人工智能之前&#xff0c;也有非常多的生成方案&#xff0c;比如如下所示&#xff0c;通过har生成接口自动化测试用例&#xff1a; 但是以上的生成方式依然是有一些弊端&#xff0c;比如 har 本身虽然能表述一定的接…

铁威马NAS教程丨TOS应用中心无法下载应用,显示0%或“准备中“?

故障排除 适用机型 所有 TNAS型号 原因分析 该现象通常是网络配置不正确或文件系统异常引起&#xff1a; 1.获取不到网关&#xff0c;状态栏甚至显示红色的“未连接” 2.路由器自动分配的DNS无法解析出下载服务器的域名 3.文件系统为只读文件系统 解决方法 1.重新获取…

中间件|day1.Redis

Redis 定义 Redis 是一个开源&#xff08;BSD许可&#xff09;的&#xff0c;内存中的数据结构存储系统&#xff0c;它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构&#xff0c;如 字符串&#xff08;strings&#xff09;&#xff0c; 散列&#xff08;hash…