C++ 特殊类的设计

devtools/2024/12/22 15:27:26/

前言

在有些开发场景下需要我们设计出一些特殊的类来满足特殊的需求,本期我们将来介绍一下常见的特殊类的设计!

目录

前言

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

二、设计一个类,只能在堆上创建对象

三、设计一个类,只能在栈上创建对象

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

五、设计一个类,只能创建一个对象(单例模式)

1、饿汉模式

2、懒汉模式


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

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

C++98

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

class CopyBan
{
public:CopyBan(){}private:// C++98 将 拷贝构造 和 赋值运算符重载 声明 为私有CopyBan(const CopyBan&);CopyBan& operator=(const CopyBan&);
};

1、为什么要将声明设置为私有?

如果将 拷贝构造 和 赋值拷贝 的声明被设置成私有,用户有可能在类外面自己实现,这样就达不到实现禁止拷贝的目的了!

2、为什么只是声明而不实现?

不实现是因为他两根本是不会被调用的,及时定义了也没啥意义。所以,不实现更简单!

C++11 :

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

class CopyBan
{
public:CopyBan(){}// C++11 将 拷贝构造 和 赋值运算符重载 让编译器删除掉CopyBan(const CopyBan&) =delete;CopyBan& operator=(const CopyBan&) = delete;
};

二、设计一个类,只能在堆上创建对象

方式一:

1、将类的构造函数私有化拷贝够声明为私有/禁用。防止别人调用拷贝构造在栈上生成对象!

2、提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建!

class HeapOnly
{
public:// 提供一个静态的 创建堆对象的成员函数static HeapOnly* CreateObject(){return new HeapOnly;}private:HeapOnly(){}HeapOnly(const HeapOnly&); // C++98 将拷贝构造的声明私有化,防止利用拷贝构造在栈上创建对象// C++11 禁用掉拷贝构造// HeapOnly(const HeapOnly&) = delete;
};

方式二:

将析构函数私有化。将析构函数私有化之后,不能直接创建对象了。只能使用 new 来创建对象即在上。现在的问题是:new 完之后如何释放呢?因为析构被设置成了私有,所以不能直接调,此时需要提供一个成员函数,在这成员函数内部直接  delete this 即可,谁调用 this 就是谁,更好解决了析构私有化不能调用的问题!

class HeapOnly
{
public:void Destroy(){delete this;}private:~HeapOnly(){cout << "~HeapOnly()" << endl;}
};

首先看一下直接创建对象不成功:

只能使用 new 创建

三、设计一个类,只能在栈上创建对象

和上面的思路类似。将构造函数私有化,然后设计一个静态的方法创建对象返回即可!

class StackOnly
{
public:static StackOnly CreateObject(){return StackOnly();}private:StackOnly(){}
};

由于没有将 new 处理所以有时候下面这种情况还是可以通过的:

int main()
{StackOnly st1 = StackOnly::CreateObject();StackOnly* st2 = new StackOnly(st1);// st2 就是堆上的对象return 0;
}

这里你可能会想:直接把拷贝构造直接禁用掉不就完全解决了嘛?表面上看好像没问题,但实际上是不行的!因为 CreateObject() 函数返回的时临时对象,所以必须是传值返回,所以不能将拷贝构造删除。

这里的解决方法是:在我们自己的类中重载 operator new 然后将他禁用掉就OK了!为什么将我们重载的 禁用掉用好了呢?原因是,我们平时的 new 是全局的,当我们自己类中实现之后默认就是用的是当前类的,这里禁用掉之后就无法 new

class StackOnly
{
public:static StackOnly CreateObject(){return StackOnly();}// 实现当前类的 专属 newvoid* operator new(size_t) = delete;private:StackOnly(){}
};

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

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

class NonTherit
{
public:static NonTherit GetInstance(){return NonTherit();}private:NonTherit(){}
};

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

class NonInherit final
{
public:NonInherit(){}
private:// ...
};

五、设计一个类,只能创建一个对象(单例模式)

设计模式:设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结/套路。

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

单例模式一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式很常用,例如我们前面实现的 线程池 就是使用的 单例模式

单例模式的两种实现:饿汉模式懒汉模式

1、饿汉模式

不管你未来用不用,在程序启动时就创建一个唯一的实例对象。

首先既然是唯一的一个实例,那必然不能拷贝,所以我么必须得把 拷贝构造 和 赋值拷贝 给删除掉。然后将 构造函数 私有化!在向外提供一个 static 的 获取该类对象的方法!

如何保障在进程启动即执行 main 时 就已将有一个创建好的对象呢?我们可以声明一个的静态成员变量(不属于类,本质是全局的),然后在类外面定义;这样就可以保证在进程启动时就已有唯一的对象了

class Singleton
{
public:static Singleton* GetInstance(){return &m_instance;}
private:// 私有化构造Singleton() {} // 禁用掉拷贝Singleton(Singleton const&) = delete;Singleton& operator=(Singleton const&) = delete;// 创建一个 static Singleton 的成员属性static Singleton m_instance;
};Singleton Singleton::m_instance;// 再类外定义

我们如何验证此时的对象只有一个呢?我们可以打印获取到单例对象的地址:

• 饿汉模式的优点:实现简单 

• 饿汉模式的缺点:可能会导致进程启动慢;如果两个单例有启动先后顺序,那么饿汉无法控制

如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。因为在程序启动前需要进行初始化,如果需要初始化的资源很多,就会降低程序的启动速度。

2、懒汉模式

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

class Singleton
{
public:static Singleton* GetInstance(){// 存在线程安全的问题--》加锁unique_lock<mutex> lock(_mtx);if (m_instance == nullptr){m_instance = new Singleton();}return m_instance;}
private:// 私有化构造Singleton() {}// 禁用掉拷贝Singleton(Singleton const&) = delete;Singleton& operator=(Singleton const&) = delete;// 声明一个 static Singleton 的成员属性static Singleton* m_instance;// 保证线程安全,声明一把互斥锁static mutex _mtx;
};Singleton* Singleton::m_instance = nullptr;// 再类外定义
mutex Singleton::_mtx;// 再类外定义

这种情况下,还可以稍微优化一下:因为枷锁还是要消耗时间的,如果是第一次还好不是第一次的话后面的每一次都要“傻傻的”加锁,在白白的消耗资源,所以我们可以在这里进行提前判断一下

static Singleton* GetInstance()
{// 双重判断if (m_instance == nullptr){// 存在线程安全的问题--》加锁unique_lock<mutex> lock(_mtx);if (m_instance == nullptr){m_instance = new Singleton();}}return m_instance;
}

• 懒汉模式的优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制

• 懒汉模式的缺点:复杂

上述的懒汉式实现的单例,有一点点小问题就是 new 的那个单例对象没有释放没可能会造成内存泄漏的问题!这里可以等进程结束的时候释放,也可以自己写一个回收机制,这里我们也实现一个简单的gc

class Singleton
{
public:static Singleton* GetInstance(){// 双重判断if (m_instance == nullptr){// 存在线程安全的问题--》加锁unique_lock<mutex> lock(_mtx);if (m_instance == nullptr){m_instance = new Singleton();}}return m_instance;}// 回收资源的 GCclass GC{public:~GC(){if (Singleton::m_instance)delete Singleton::m_instance;}};static GC gc;// 定义一个静态成员变量,程序结束时,系统自动调用它的析构释放资源
private:// 私有化构造Singleton() {}// 禁用掉拷贝Singleton(Singleton const&) = delete;Singleton& operator=(Singleton const&) = delete;// 声明一个 static Singleton 的成员属性static Singleton* m_instance;// 保证线程安全,声明一把互斥锁static mutex _mtx;
};Singleton* Singleton::m_instance = nullptr;// 再类外定义
mutex Singleton::_mtx;// 再类外定义

我们这次使用多线程测试一下:

int main()
{thread t1([] {cout << Singleton::GetInstance() << endl; });thread t2([] {cout << Singleton::GetInstance() << endl; });t1.join();t2.join();cout << Singleton::GetInstance() << endl;cout << Singleton::GetInstance() << endl;return 0;
}


OK,本期分享就到这里,我是 CP 我们下期再见~!


http://www.ppmy.cn/devtools/144407.html

相关文章

前端开发 详解 Node. js 都有哪些全局对象?

在 Node.js 中&#xff0c;全局对象&#xff08;Global Objects&#xff09;是指在任何模块中都可以直接访问的对象和变量&#xff0c;而不需要显式地进行导入。Node.js 提供了一些全局对象&#xff0c;帮助开发者在编写应用程序时更加方便地进行一些常见操作&#xff0c;如文件…

头歌实训数据结构与算法-图的最短路径(第2关:多源最短路径)

任务描述&#xff1a; 在带权有向图G中&#xff0c;求G中的任意一对顶点间的最短路径问题&#xff0c;也是十分常见的一种问题。 解决这个问题的一个方法是执行n次迪杰斯特拉算法&#xff0c;这样就可以求出每一对顶点间的最短路径&#xff0c;执行的时间复杂度为O(n 3)。而另…

企业内训|阅读行业产品运营实战训练营-某运营商数字娱乐公司

近日&#xff0c;TsingtaoAI公司为某运营商旗下数字娱乐公司组织的“阅读行业产品运营实战训练营”在杭州落下帷幕。此次训练营由TsingtaoAI资深互联网产品专家程靖主持。该公司的业务骨干——来自内容、市场、业务、产品与技术等跨部门核心岗位、拥有8-10年实战经验的中坚力量…

Linux系统加固

Linux系统安全加固 文章目录 Linux系统安全加固密码策略文件、目录安全未授权suid、未授权sgid排查与加固禁止root登录ftp、禁止匿名访问ftp计划任务排查与加固、开机自启排查与加固限定root用户远程ssh登录日志加固 无用账号、用户组和空口令账户排查与加固 禁用或删除无用账号…

WPF依赖属性详解

在 WPF 中&#xff0c;依赖属性&#xff08;Dependency Property&#xff09;是一种特殊的属性类型&#xff0c;它提供了比普通属性更强大的功能。依赖属性是 WPF 数据绑定、样式、动画和模板等功能的基础。理解依赖属性是理解 WPF 复杂功能和性能优化的关键。 1. 依赖属性是什…

【LuaFramework】服务器模块相关知识

目录 一、客户端代码 二、本地服务器代码 三、解决服务器无法多次接收客户端消息问题 一、客户端代码 连接本地服务器127.0.0.1:2012端口&#xff08;如何创本地服务器&#xff0c;放最后说&#xff09;&#xff0c;连接成功后会回调 协议号Connect是101&#xff0c;其他如下…

clickhouse优化记录

一、注重使用分区键来加快查询 在大数据量的情况下&#xff0c;如果查询语句中&#xff0c;可以使用分区键来进行查询&#xff0c;可以极大缩小数据的查询范围&#xff0c;加快查询速度。 二、使用order by的列&#xff0c;适用最左前缀匹配原则 比如表的结构是 order by(id…

深入理解Android WebView的加载流程与事件回调

在Android开发中&#xff0c;WebView用于显示网页和执行JavaScript。理解其加载流程和事件回调对于开发一个功能丰富且用户友好的基于Web的应用至关重要。本文将详细介绍 WebView 加载一个URL时的整个流程和相关的事件回调&#xff0c;帮助开发者更好地掌握其使用方法和处理可能…