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

目录

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.mfbz.cn/a/871845.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024年入职/转行网络安全,该如何规划?_网络安全职业规划

前言 前段时间&#xff0c;知名机构麦可思研究院发布了 《2022年中国本科生就业报告》&#xff0c;其中详细列出近五年的本科绿牌专业&#xff0c;其中&#xff0c;信息安全位列第一。 网络安全前景 对于网络安全的发展与就业前景&#xff0c;想必无需我多言&#xff0c;作为…

探索802.1X:构筑安全网络的认证之盾

在现代网络安全的世界里&#xff0c;有一个极其重要但又常常被忽视的角色&#xff0c;它就是802.1x认证协议。这个协议可以被称作网络安全的守护者&#xff0c;为我们提供了强有力的防护。今天&#xff0c;我们就来深入探讨一下802.1x的原理、应用和测试&#xff0c;看看它是如…

干货|光伏开发全流程

在当今全球能源转型与应对气候变化的背景下&#xff0c;光伏产业作为可再生能源的重要组成部分&#xff0c;正以前所未有的速度发展。光伏开发&#xff0c;即从项目规划到并网发电的全过程&#xff0c;涉及多个环节&#xff0c;每个步骤都至关重要。而其中户用和工商业光伏开发…

DBAPI如何用SQL将多表关联查询出树状结构数据(嵌套JSON格式)

场景描述 假设数据库中有3张表如下&#xff1a; 客户信息表 订单表 订单详情表 一个客户有多个订单&#xff0c;一个订单包含多个产品信息&#xff0c;客户-订单-产品就构成了3级的树状结构&#xff0c;如何查询出如下树状结构数据呢&#xff1f; [{"customer_age"…

Notion使用详解

​ ​ 您好&#xff0c;我是程序员小羊&#xff01; 前言 Notion是一款集笔记、任务管理、知识库、文档协作等多功能于一体的生产力工具。其灵活性和可定制性使得它在个人和团队协作中都非常受欢迎。本教程将详细介绍如何使用Notion的基本功能&#xff0c;帮助你快速上手并充分…

【云原生】Ingress控制器超级详解

Ingress资源对象 文章目录 Ingress资源对象一、Ingress1.1、Ingress是什么&#xff1f;1.2、Ingress术语1.3、Ingress类型 二、Ingress详细2.1、部署Nginx-Ingress控制器2.2、最小Ingress资源2.3、Ingress规则 三、一个域名多个访问路径多SVC四、多域名Ingress五、转发到默认Se…

【C语言小项目】五子棋游戏

目录 前言 一、游戏规则 1.功能分析 2.玩法分析 3.胜负判定条件 二、游戏实现思路 三、代码实现与函数封装 1.项目文件创建 2.头文件说明 3.函数封装 1&#xff09;菜单实现 2&#xff09;进度条实现 3&#xff09;main函数实现 4&#xff09;Game函数 5&#xff0…

【系统架构设计】软件架构设计(2)

【系统架构设计】软件架构设计&#xff08;1&#xff09; 软件架构概述架构需求与软件质量属性软件架构风格层次系统架构风格面向服务的架构SOA概述微服务微服务和SOA差异 软件架构概述 架构需求与软件质量属性 软件架构风格 层次系统架构风格 面向服务的架构 SOA概述 面…

C语言手撕实战代码_循环单链表和循环双链表

C语言手撕实战代码_循环单链表和循环双链表 循环单链表习题1.建立带头结点的循环链表2.设计一个算法&#xff0c;将一个带有头结点的循环单链表中所有结点的链接方向逆转3.设计一个算法,将一个循环单链表左移k个结点4.设计一个算法将循环单链表中的结点p的直接前驱删除5.设计算…

游泳耳机品牌前十名哪个牌子好?如何选高配游泳耳机不花冤枉钱?

在快节奏的现代生活中&#xff0c;音乐已成为许多人放松和充电的重要方式之一。无论是晨跑、通勤还是健身&#xff0c;音乐总能陪伴我们度过每一个瞬间。而对于游泳爱好者来说&#xff0c;能够在水中享受音乐&#xff0c;更是将这一运动提升到了一个新的层次。然而&#xff0c;…

uniapp/uniapp x总结

uni-app组成和跨端原理 上图所诉 App的渲染引擎&#xff1a;同时提供了2套渲染引擎&#xff0c;.vue页面文件由webview渲染&#xff0c;原理与小程序相同&#xff1b;.nvue页面文件由原生渲染&#xff0c;原理与react native相同。开发者可以根据需要自主选择渲染引擎。 uniapp…

【unity小技巧】获取免费开源的人物模型,并为obj fbx人物模型绑定骨骼、动画——mixamo的使用介绍

文章目录 前言地址上传自己的3D角色下载单动画下载动作包角色模型导入Unity动画导入unity设置动画骨骼动画骨骼不配的问题参考完结 前言 其实前面我已经推荐了几种获取人物模型的方法&#xff1a; 1、【unity小技巧】下载原神模型&#xff0c;在Blender中PMX模型转FBX模型&…

多商户商品下单限购问题修复

问题: 当商品设置限购为 1 时,够买数量未超过限购,但是还是提示超出限购数量 修复方法: 修改代码路径: app\common\repositories\store\order\StoreOrderCreateRepository.php 修改代码一: i s p a y s a r r a y u n i q u e ( a r r a y c o l u m n ( is_pays array_un…

Python 设置Excel工作表页边距、纸张大小/方向、打印区域、缩放比例

在使用Excel进行数据分析或报告制作时&#xff0c;页面设置是确保最终输出效果专业、美观的关键步骤。合理的页面设置不仅能够优化打印效果&#xff0c;还能提升数据的可读性。本文将详细介绍如何使用Python操作Excel中的各项页面设置功能。 目录 Python 设置Excel工作表页边…

AutosarMCAL开发——基于EB FEE驱动

这目录 1. FEE原理2.EB配置以及接口应用3.总结 1. FEE原理 在Fls解析文章中介绍了Flash与EEPROM储存器的区别&#xff0c;本文将介绍FEE具体实现原理。 FEE模块&#xff0c;全称Flash EEPROM Emulation Module&#xff0c;旨在使用Flash模拟EEPROM以增加使用寿命。 术语解释 p…

CTFHUB | web进阶 | JSON Web Token | 无签名

一些JWT库也支持none算法&#xff0c;即不使用签名算法。当alg字段为空时&#xff0c;后端将不执行签名验证 开启题目 账号密码随便输&#xff0c;登录之后显示只有 admin 可以获得 flag 在此页面抓包发到 repeater&#xff0c;这里我们需要用到一个 Burp 插件&#xff0c;按图…

瑞吉外卖-登录时报错:接口404异常

一、错误描述 出现“系统接口404异常”的弹窗&#xff0c;同时一直显示登录中&#xff0c;而无法跳转到后台页面。 二、解决方法 1. 检查浏览器的网址 确保为localhost:8080/backend/page/login/login.html&#xff0c;而不是idea自动生成的&#xff0c;修改过来即可。 2.确…

Unity XR Interaction Toolkit 通过两个手柄控制物体放大缩小

1&#xff1a;给物体添加 XR General Grab Transformer 脚本 2&#xff1a;XR Grab Interactable 的 select mode 选择 Multiple

SpringIoCDI

前言&#x1f440;~ 上一章我们介绍了Spring MVC&#xff0c;今天介绍Spring核心功能之一IoC Spring到底是什么&#xff1f; Spring IoC 什么是 IoC 容器&#xff1f; IoC 介绍 DI 介绍 IoC详解 获取Bean对象的其他方式 Bean的存储 方法注解 Bean 扫描路径 DI详解 …

拼图游戏02

文章目录 概要整体架构流程代码过程小结 概要 现在需要将图片添加界面中 关键点在于它如何动态地根据游戏状态更新用户界面。它使用了Swing的布局管理器来定位组件&#xff0c;并且通过ImageIcon和JLabel来显示图像。注意&#xff0c;路径字符串中的反斜杠在Java中是转义字符…