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.rhkb.cn/news/402174.html

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

相关文章

RTX 4070 GDDR6显存曝光:性能与成本的平衡之选

近期&#xff0c;关于NVIDIA RTX 4070新显卡的信息曝光&#xff0c;这款显卡将配备较为缓慢的GDDR6显存&#xff0c;而非更高性能的GDDR6X。这一配置的选择引发了业内的广泛关注&#xff0c;特别是在性能与成本的平衡问题上。 新版RTX 4070 OC 2X的核心特点 **1.显存类型与带…

8B 端侧小模型 | 能力全面对标GPT-4V!单图、多图、视频理解端侧三冠王,这个国产AI开源项目火爆全网

这两天&#xff0c; Github上一个 国产开源AI 项目杀疯了&#xff01;一开源就登上了 Github Trending 榜前列&#xff0c;一天就获得将近600 star。 这个项目就是国内大模型四小龙之一面壁智能最新大打造的面壁「小钢炮」 MiniCPM-V 2.6 。它再次刷新端侧多模态天花板&#xf…

微分方程求解的三种解析方法:经典时域法(齐次解+特解,零状态+零输入),冲激响应卷积法、传递函数法

经典时域分析方法 以例题的形式对经典时域解法&#xff08;齐次解特解&#xff09;进行说明&#xff0c;最后进行总结。考虑如下形式微分方程&#xff1a; y ′ ′ ( t ) 5 y ′ ( t ) 6 y ( t ) 2 f ′ ( t ) 6 f ( t ) y\left( t \right) 5y\left( t \right) 6y\left(…

pyinstaller使用

pyinstaller 入门 Pyat5 的安装程序开发PyQt6 的安装程序开发 编写好的程序编译成可执行文件资源文件:用 zip 打包&#xff0c;基本可以压缩到 1/3 大小;然后再用 pyqt 写一个 setup 安装程序&#xff0c;安装到指定目录(安装的过程实际就是把文件解压、拷贝到指定目录、注册到…

[000-01-030].第2节 :Zookeeper本地安装

1.Zookeeper下载地址 1.Zookeeper官网地址 2.会显示Zookeeper的一些版本 2.Zookeeper本地模式安装&#xff1a; 2.1.Zookeeper安装前准备 1.在Centos7虚拟机中安装jdk8 2.2.Zookeeper安装过程&#xff1a; 1.下载zookeeper压缩版本&#xff0c;解压放在opt/moduel目录下…

虚拟人实时主持创意互动方案:赋能峰会论坛会议等活动科技互动感

随着增强现实、虚拟现实等技术的不断发展&#xff0c;“虚拟人实时主持”创意互动模式逐渐代替传统单一真人主持模式&#xff0c;虚拟主持人可以随时随地出现在不同活动现场&#xff0c;也可以同一时间在不同分会场中担任主持工作&#xff0c;在峰会、论坛、会议、晚会、发布会…

计算机网络三级笔记--原创 风远 恒风远博

典型设备中间设备数据单元网络协议物理层中继器、集线器中继器、集线器数据位(bit) binary digit二进 制数据的缩写HUB使用了光纤、 同轴电缆、双绞 线.数据链路层网卡、网桥、交换机网桥、交换机数据帧(Frame)STP、ARQ、 SW、CSMA/CD、 PPP(点对点)、 HDLC、ATM网络层路由器、…

MySQL 管理

启动及关闭 MySQL 服务器 Windows 系统下 启动 MySQL 服务器&#xff1a; 1、通过 “服务” 管理工具&#xff1a; 打开“运行”对话框&#xff08;Win R&#xff09;&#xff0c;输入 services.msc&#xff0c;找到“MySQL”服务&#xff0c;右击选择“启动”。 2、通过命…

汇量科技Mintegral发布全新产品矩阵:助力广告主高效增长与变现

近期&#xff0c;汇量科技旗下程序化互动式广告平台Mintegral正式推出全新产品命名&#xff0c;期望通过简洁明确的产品名称&#xff0c;更好地传达Mintegral的品牌理念&#xff0c;使客户与平台的每一次接触都更加直接高效。 Mintegral AppGrowth(原Mintegral Self-Service Pl…

QLabel设置图像的方法+绘制文本换行显示

1、QLabel设置图像有两种方法 (1) void setPicture(const QPicture &); (2) void setPixmap(const QPixmap &); QPicture和QPixmap都是继承于QPaintDevice&#xff0c;它们都可以通过加载图片的方式获取&#xff1a;bool load(QIODevice *dev, const char *format …

【直播预告】智能机器人赛道技术培训定档8.20

在不远的将来&#xff0c;机器人可能会成为我们日常生活中不可或缺的伙伴&#xff0c;它们在工业生产线上精准操作&#xff0c;在家庭中提供温馨陪伴&#xff0c;甚至在探索未知领域中担当先锋。而现在&#xff0c;正是我们拥抱这一未来&#xff0c;深入了解并掌握智能机器人技…

【Python机器学习】树回归——树剪枝

如果一棵树节点过多&#xff0c;表明该模型可能对数据进行了过拟合。 通过降低决策树的复杂度来避免过拟合的过程称为剪枝。提过提前终止条件&#xff0c;实际上就是在进行一种所谓的预剪枝&#xff1b;另一种形式的剪枝需要使用测试集和训练集&#xff0c;称作后剪枝。 预剪…

PMP到底有什么用?

PMP 就是项目管理证书&#xff0c;全称是项目管理专业人士资格认证&#xff0c;对于一个在项目管理岗位混迹五年的老油条来说&#xff0c;PMP 证书是敲开项目管理岗位的第一块砖&#xff0c;每年考 PMP 的人都很多&#xff0c;要是 PMP 证书没有价值&#xff0c;还会有那么多人…

c语言-经典例题

C语言-经典例题 一、单项选择题 1、 -- A 2、 -- C y<5 --是关系运算符的优先级大于&& -- 是逻辑运算符 3、 -- B - D选项&#xff1a;c是float类型&#xff0c;所以c/2是1.5 4、 -- C 从后往前执行&#xff08;先算后面的&a…

利用住宅代理应对机器人流量挑战:识别、使用与检验指南

引言 什么是机器人流量&#xff1f;其工作原理是什么&#xff1f; 机器人流量来自哪里&#xff1f; 合法使用机器人时如何避免被拦截&#xff1f; 如何检验恶意机器人流量&#xff1f; 总结 引言 你是否曾经遇到过访问某个网站时&#xff0c;被要求输入验证码或完成一些其…

时光荏苒:中年之际的自我追寻

余华在《活着》写到&#xff1a;“曾经以为老去是很遥远的事&#xff0c;突然发现年轻已经是很久以前的事了&#xff0c;时光好不经用&#xff0c;抬眼已是半生&#xff0c;所谓的中年危机&#xff0c;真正让人焦虑的不是孤单&#xff0c;不是贫穷&#xff0c;更不是衰老&#…

汽车EDI:法雷奥Valeo EDI项目案例

Valeo是一家总部位于法国的汽车零部件供应商。它专注于设计、生产、和销售各种创新产品和系统&#xff0c;以提高汽车的能效和减少排放。其业务主要分为舒适与驾驶辅助系统、动力总成系统、热系统以及可视系统。 本文将从业务的角度出发&#xff0c;带领大家了解供应商H公司在对…

类加载与双亲委派

类加载 reference: https://docs.oracle.com/javase/tutorial/ext/basics/load.html bootstrap classloader&#xff1a;引导&#xff08;也称为原始&#xff09;类加载器&#xff0c;它负责加载 Java 的核心类。这个加载器是非常特殊的&#xff0c;它实际上不是 java.lang.Cla…

Linux网络编程-----协议

1.协议 通信双方约定的一套标准 2.国际网络通信协议标准&#xff1a; 1.OSI协议&#xff1a;&#xff08;过于冗余&#xff09; 应用层 发送的数据内容 表示层 数据是否加密 会话层 是否建立会话连接 传输层 数据…

buuctf [2019红帽杯]easyRE

前言&#xff1a;学习笔记。&#xff08;玩了几天。。&#xff09; 常规&#xff1a;下载 解压 查壳 64位 >>> 64IDAPro打开。 先看字符串&#xff0c;这个没有 main函数。 进去看看函数。 分析&#xff1a; 汇编看>>>连续引用传送 说明 实际上其实就是数组…