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.mzph.cn/web/51015.shtml

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

相关文章

2 种方式申请免费 SSL 证书,阿里云 Certbot

如何使用免费的 SSL 证书&#xff0c;有时在项目中需要使用免费的 SSL 证书&#xff0c;Aliyun 提供免费证书&#xff0c;三个月有效期&#xff0c;可以直接在aliyun 申请&#xff0c;搜索 SSL 证书&#xff0c;选择测试证书。 Aliyun 证书需要每三月来来换一次&#xff0c;页…

【学习笔记】8、脉冲波形的变换与产生

本章简略记录。 8.1 单稳态触发器&#xff08;脉冲触发&#xff09; 单稳态触发器 应用于 &#xff1a;&#xff08;1&#xff09;脉冲整型&#xff08;2&#xff09;脉冲延时 &#xff08;3&#xff09;定时 单稳态触发器的工作特性&#xff1a; 没有触发脉冲作用时&#xf…

Vue项目-三级联动的路由跳转与传参

三级联动组件的路由的跳转与传参 三级联动&#xff0c;用户可以点击的&#xff1a;一级分类、二级分类和三级分类 以商城项目为例&#xff0c;Home模块跳转到Search模块&#xff0c;以及会把用户选中的产品&#xff08;产品名字、产品ID&#xff09;在路由跳转的时候&#xff…

《黑神话.悟空》:一场跨越神话与现实的深度探索

《黑神话.悟空》&#xff1a;一场跨越神话与现实的深度探索 在国产游戏日益崛起的今天&#xff0c;《黑神话.悟空》以其独特的剧情、丰富的人物设定和深刻的主题&#xff0c;成为了无数玩家翘首以盼的国产3A大作。这款游戏不仅是一次对传统故事的创新演绎&#xff0c;更是一场对…

操作系统:内存管理策略

外部碎片 当应用程序启动时&#xff0c;由操作系统负责给该应用程序分配其内存空间&#xff0c;假设此时启动了三个应用程序&#xff0c;操作系统分别给其分配了100m&#xff0c;10m和50m的内存&#xff0c;内存情况如下图 此时如果程序B下线&#xff0c;程序A和程序C之间便会…

【css】伪元素实现图片个悬停文字聚焦效果

实现重点&#xff1a; 文字覆盖在图片上&#xff1a; 通过使用 position: absolute 将 .box 文字盒子定位在图片上方。父容器 .img-wrap 使用了 position: relative 确保子元素的绝对定位在父容器的边界内生效。 创建悬停效果&#xff1a; 通过使用 &::before 和 &::…

国富基金入股的关联性与奇瑞依赖症,大昌科技业务独立性引关注

《港湾商业观察》廖紫雯 日前&#xff0c;安徽大昌科技有限公司&#xff08;以下简称&#xff1a;大昌科技&#xff09;更新招股书并完成三轮问询&#xff0c;公司冲刺深交所创业板得到进一步进展。此前&#xff0c;2023年6月&#xff0c;大昌科技IPO获深交所受理&#xff0c;…

【微信小程序】自定义组件 - 数据、方法和属性

1. data 数据 2. methods 方法 在小程序组件中&#xff0c;事件处理函数和自定义方法需要定义到 methods 节点中&#xff0c;示例代码如下&#xff1a; 3. properties 属性 在小程序组件中&#xff0c;properties 是组件的对外属性&#xff0c;用来接收外界传递到组件中的数…

杰发科技AC7840——CAN通信简介(8)_通过波特率和时钟计算SEG_1/SEG_2/SJW/PRESC

通过公式计算 * 波特率和采样点计算公式&#xff1a;* tSeg1 (S_SEG_1 2); tSeg2 (S_SEG_2 1).* BandRate (SCR_CLK / (S_PRESC 1) / ((S_SEG_1 2) (S_SEG_2 1))), SCR_CLK 为CAN 模块源时钟* SamplePoint (tSeg1 / (tSeg1 tSeg2)). {0x0D, 0x04, 0x04, 0x3B},…

数据结构——链式队列和循环队列

目录 引言 队列的定义 队列的分类 1.单链表实现 2.数组实现 队列的功能 队列的声明 1.链式队列 2.循环队列 队列的功能实现 1.队列初始化 (1)链式队列 (2)循环队列 (3)复杂度分析 2.判断队列是否为空 (1)链式队列 (2)循环队列 (3)复杂度分析 3.判断队列是否…

26.删除有序数组中的重复项---力扣

题目链接&#xff1a; . - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/remove-duplicates-from-sorted-array/descript…

仿Muduo库实现高并发服务器——Server.hpp框架的简单描述

EventLoop模块在本项目中的简单使用&#xff1a; 下面这张图 是channel模块&#xff0c;poller模块&#xff0c;TimerWheel模块&#xff0c;EventLoop模块&#xff0c;LoopThreadPool模块进行组合。便于大家对这个项目的理解&#xff0c;因为代码看起来挺复杂的。 下面这个图&…

招募活动投稿展示 | 感受科技温度,从一个 LLM 应用开始

活动介绍 谷歌开发者招募活动是专为 Google 技术的爱好者及开发者们开展的活动&#xff0c;旨在鼓励大家通过多种形式 (文章/视频/coding 等) 创作与 Google 技术相关的讲解分享、实践案例或活动感受等内容&#xff0c;展示代码、框架、平台在真实世界中的生动表现&#xff0c;…

详解Spring Bean的生命周期

详解Bean的生命周期 前言 在我们没有使用Spring框架之前&#xff0c;创建对象一般都是使用new关键字进行创建&#xff0c;当然除了new关键字外&#xff0c;还有 运用反射手段&#xff0c;使用Class类的newInstance方法 或者 Constructor类中的newInstance方法使用clone方法使…

JavaScript语法基础之DOM基础

目录 1. DOM 基础 1.1. DOM 是什么&#xff1f; 1.1.1. DOM 对象 1.1.2. DOM 结构 1.2. 节点类型 1.3. 获取元素 1.3.1. getElementById() 1.3.2. getElementsByTagName() 1.3.3. getElementsByClassName() 1.3.4. getElementsByName() 1.4.如何去操作对象 修改属性…

IP SSL证书的未来趋势:适应不断变化的安全挑战

随着网络攻击手段的不断进化和用户对隐私保护意识的增强&#xff0c;IP SSL证书作为保障网络安全的关键组件之一&#xff0c;也在不断地发展和完善。本文将探讨IP SSL证书的未来趋势&#xff0c;以及如何适应这些不断变化的安全挑战。 当前状况与挑战 网络安全意识提升&#…

ARM 裸机与 Linux 驱动对比及 Linux 内核入门

目录 ARM裸机代码和驱动的区别 Linux系统组成 内核五大功能 设备驱动分类 内核类型 驱动模块 驱动模块示例 Makefile配置 命令 编码辅助工具 内核中的打印函数 printk 函数 修改打印级别 ​编辑 打印级别含义 驱动多文件编译 示例 模块传递参数 命令行传递参数…

python-docx 实现 Word 办公自动化

前言&#xff1a;当我们需要批量生成一些合同文件或者简历等。如果手工处理对于我们来说不仅工作量巨大&#xff0c;而且难免会出现一些问题。这个时候运用python处理word实现自动生成文件可极大的提高工作效率。 python-docx是python的第三方插件&#xff0c;用来处理word文件…

Kubectl命令、初识pod、namespace

文章目录 一、Kubectl简介基础命令1.基本信息命令2.创建和更新资源命令3.删除资源命令4. 查看日志和调试命令5. 端口转发和复制文件命令6. 部署管理命令7. 伸缩命令8. 配置和上下文管理命令9.常用命令 二、Pod简介核心概念pod常见状态调度和初始化阶段容器创建和运行阶段异常状…

Qt网络通信——TCP和UDP

一、TCP通信 TCP通信必须先建立 TCP 连接&#xff0c;通信端分为客户端和服务器端。 Qt 为服务器端提供了 QTcpServer 类用于实现端口监听&#xff0c;QTcpSocket 类则用于服务器和客户端之间建立连接。大致流程如下图所示&#xff1a; 1. 服务器端建立 1.1 监听——listen() …