【C++】特殊类实现

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

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

  • C++98

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

class CopyBan
{// ...private:CopyBan(const CopyBan&);CopyBan& operator=(const CopyBan&);//...
};

原因:
        1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了
        2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写
反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。

  • C++11

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

class CopyBan
{// ...CopyBan(const CopyBan&)=delete;CopyBan& operator=(const CopyBan&)=delete;//...
};

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

方法一:将类的析构函数私有。防止别人调用拷贝在栈上生成对象

编译报错:

栈上创建会自动调用析构,这里析构被私有了,就不能在栈上创建。

但这样在堆上创建后要进行delete也要调用析构函数也就是虽然不能在栈上创建对象,但是同样也不能在堆上创建对象了

解决方法:在类public中实现一个成员函数进行delete

class HeapOnly
{
public:void Destroy(){delete this;}
private:~HeapOnly(){//...}
};int main()
{//HeapOnly hp1;//static HeapOnly hp2;HeapOnly* hp3 = new HeapOnly;//delete hp3;hp3->Destroy();return 0;
}

编译成功:

方法二:

实现方式:
1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。

完整代码:

class HeapOnly
{
public:static HeapOnly* CreateObj(){return new HeapOnly;}
private:HeapOnly(){//...}HeapOnly(const HeapOnly& hp) = delete;HeapOnly& operator=(const HeapOnly& hp) = delete;
};int main()
{HeapOnly* hp3 = HeapOnly::CreateObj();//HeapOnly copy(*hp3);return 0;
}

以上代码有两个细节:

1. 如果没有提供静态的成员函数:

会发生报错,调用一个类的普通成员函数是不是需要对象去调用,那我们在堆上申请的对象又正好需要调用这个函数来生成,可是我们选择都没有对象我们如何去调用这个函数呢?所以这就纯纯是先有鸡还是先有蛋的问题了。

为了解决这个问题,我们可以将这个成员函数设置成静态的

2. 利用拷贝构造在栈上创建报错:

虽然我们不能够直接的在栈上创建对象,但是我们可以在堆上申请一个对象之后再调用一次拷贝构造然后间接的就在栈上申请对象了。

所以我们的完整代码还要将拷贝构造和赋值重载都声明成delete禁掉,所以这里调用拷贝构造报错

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

方法:同上将构造函数私有化,然后设计静态方法创建对象返回即可。

下面先来看这种方式的实现:

编译报错,说明不能在堆上创建。

但是我们如果在new的时候调用拷贝构造:

编译通过:

我们发现又可以在堆上成功了,说明这种方法还有漏洞。

解决方法:下面我们来分析一下:new是由operator new + 构造组成的,所以我们只要在类中重载一个new,并将new声明成delete,就可以解决这个问题。

class StackOnly
{
public:static StackOnly CreateObj(){StackOnly st;return st;}
private:StackOnly(){//...}// 对一个类实现专属operator newvoid* operator new(size_t size) = delete;
};

运行发现报错,说明不能只有new在堆上创建对象了。

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

  • C++98方式

将构造函数私有:

基类的私有成员在派生类不可访问,而派生类要继承,需要调用基类的构造函数。所以将构造函数私有可以,让类不能被继承。

// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:static NonInherit GetInstance(){return NonInherit();}
private:NonInherit(){}
};
  • C++11方法

final关键字,final修饰类,表示该类不能被继承。

class A  final
{// ....
};

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

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

为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。
设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

对于单例模式我们首先需要解决一个问题:如果保证全局(一个进程中)只有一个唯一实例对象

经过上面的学习我们就可以知道,要想保证全局只有一个唯一实例对象我们需要做以下两步操作

  • 构造函数私有定义拷贝构造和赋值防拷贝禁掉
  • 提供一个GetInstance获取单例对象

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

饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象

下面来看饿汉模式的代码:

主要有以下三个关键步骤

1、构造函数私有   2、提供获取单例对象的接口函数   3、防拷贝

经过上面的学习我们就可以知道,为了防止被创建多个对象,所以我们要1、3这俩步,构造函数私有定义拷贝构造和赋值防拷贝禁掉。

然后我们创建单例对象, 还要给它提供获取单例对象的接口函数。

// 饿汉模式:一开始(main函数之前)就创建单例对象
// 提供一个静态指向单例对象的成员指针,初始化时new一个对象给它
// 1、如果单例对象初始化内容很多,影响启动速度
// 2、如果两个单例类,互相有依赖关系。 
// 假设有A B两个单例类,要求A先创建,B再创建,B的初始化创建依赖A
namespace hungry
{class Singleton{public:// 2、提供获取单例对象的接口函数static Singleton& GetInstance(){return _sinst;}void func();void Add(const pair<string, string>& kv){_dict[kv.first] = kv.second;}void Print(){for (auto& e : _dict){cout << e.first << ":" << e.second << endl;}cout << endl;}private:// 1、构造函数私有Singleton(){// ...}// 3、防拷贝Singleton(const Singleton& s) = delete;Singleton& operator=(const Singleton& s) = delete;map<string, string> _dict;// ...static Singleton _sinst;};Singleton Singleton::_sinst;void Singleton::func(){// _dict["xxx"] = "1111";}
}int main()
{//Singleton s1;//Singleton s2;cout << &hungry::Singleton::GetInstance() << endl;cout << &hungry::Singleton::GetInstance() << endl;cout << &hungry::Singleton::GetInstance() << endl;//Singleton copy(Singleton::GetInstance());hungry::Singleton::GetInstance().Add({ "xxx", "111" });hungry::Singleton::GetInstance().Add({ "yyy", "222" });hungry::Singleton::GetInstance().Add({ "zzz", "333" });hungry::Singleton::GetInstance().Add({ "abc", "333" });hungry::Singleton::GetInstance().Print();return 0;
}

运行结果:

可以看到单例模式只能创建了一个对象

把其他的创建屏蔽掉再看运行结果:

可以看到GetInstance()之后地址都是相同的,说明只创建了一个对象。并且能实现里面简单的功能

饿汉模式适用场景:

  • 如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。

饿汉模式不适用场景:

  • 如果单例类构造函数中,要做很多配置初始化工作,导致程序启动非常慢
  • 如果两个单例类,互相有依赖关系。 饿汉模式也会有问题。(比如有A和B两个单例类,要求A单例先初始化,B必须在A之后初始化。那么饿汉无法保证。

这个时候我们使用饿汉就不合适了,这时我们应该使用下面的懒汉模式。

懒汉模式

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

下面来看这段懒汉的代码


namespace lazy
{//懒汉模式class Singleton{public:static Singleton* GetInstance(){if (_psinst == nullptr){_psinst = new Singleton;}return _psinst;}void Print(){cout << "Print()" << _a << endl;}private:Singleton():_a(0){}//C++98 防拷贝//Singleton(const Singleton&);//Singleton& operator=(const Singleton&);//C++11 防拷贝Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;int _a;static Singleton* _psinst;};Singleton* Singleton::_psinst = nullptr;}

但是这么写是会存在一些问题的。

在多线程场景下会存在线程安全的问题,因为我们的懒汉式要用的时候才会去调用GetInstance函数再去创建那个唯一的对象。

假如说现在有两个线程,分别是线程A与线程B,然后我线程A与线程B都调用了GetInstance函数线程A与线程B都进入了if条件判断里面,此时我们线程A如果先执行实例化对象的代码,然后返回。因为我们的线程B不知道线程A已经实例化了唯一对象,此时线程B再去调用实例化对象的代码,就会导致前面的唯一对象的地址被覆盖掉线程A的数据会丢失,并且会有内存泄漏的风险,更严重的是此时已经产生了两个实例对象,违反了单例模式的设计思想。

那我们应如何解决上面的问题呢?我们需要进行双检查加锁。

//懒汉模式
#include <mutex> 
namespace lazy
{class Singleton{public:static Singleton* GetInstance(){// 保护第一次需要加锁,后面都不需要加锁的场景,可以使用双检查加锁// 特点:第一次加锁,后面不加锁,保护线程安全,同时提高了效率if (_pinst == nullptr){_mtx.lock();if (_pinst == nullptr){_pinst = new Singleton;}_mtx.unlock();}return _pinst;}static void DelInstance(){_mtx.lock();if (_pinst){delete _pinst;_pinst = nullptr;}_mtx.unlock();}void Print(){cout << "Print()" << _a << endl;}private:Singleton():_a(0){// 假设单例类构造函数中,要做很多配置初始化}~Singleton(){// 程序结束时,需要处理一下,持久化保存一些数据}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;// 实现一个内嵌垃圾回收类    class CGarbo {public:~CGarbo(){if (_pinst){delete _pinst;_pinst = nullptr;}}};int _a;static Singleton* _pinst;static std::mutex _mtx;static CGarbo _gc;};Singleton* Singleton::_pinst = nullptr;std::mutex Singleton::_mtx;Singleton::CGarbo Singleton::_gc;
}

为什么是双检查加锁?

我们需要知道我们想加锁的地方只是第一次 _pinst为空的时候,我们想实例化一个唯一对象时才需要加锁,但是如果到了后面我们已经实例化出了对象,但是我们照样加锁了之后再去判断 _pinst是否为空,然后在多线程场景下频繁的加锁解锁会导致效率降低,并且也不推荐这样做。

而我们的双检查加锁里面的第二个if检查就是我先进行加锁,然后进行判断第一次_pinst是否为空然后实例化一个唯一对象等到下一次某个线程再调用GetInstance的时候,我们的第一个if检查发现pinst不为空,就不会进入第一个if条件判断的里面因此也就不存在多线程场景下频繁加锁解锁,保护线程安全,同时提高了效率。
 

饿汉模式和懒汉模式的优缺点对比如下:

饿汉模式:

  1. 优点:实例化对象在类加载时完成,因此无须考虑多线程访问问题,可以确保实例的唯一性。
  2. 缺点:
  • 饿汉式单例对象在类加载时就被创建,可能会造成系统资源的浪费,尤其是在对象占内存较大或对象初始化耗时较长的情况下。
  • 如果有多个单例对象,他们之间有初始化依赖关系,饿汉模式也会有问题。(比如有A和B两个单例类,要求A单例先初始化,B必须在A之后初始化。那么饿汉无法保证。这种场景下面用懒汉就可以,懒汉可以先调用A::GetInstance(),再调用B::GetInstance().)

懒汉模式:

  1. 优点:实现了延迟加载,只有在第一次使用时才创建实例,节省了系统资源。(解决饿汉的缺点。因为他是第一次调用GetInstance时创建初始化单例对象)
  2. 缺点:必须考虑与处理多个线程同时访问的问题,需要进行双重检查锁定等机制及进行控制。

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

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

相关文章

美团面试:Oracle JDK那么好,为何要用Open JDK?

说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格&#xff0c;遇到很多很重要的面试题&#xff1a; 既然 Oracle JDK 这么好&#xff0c;那为什么还要有 OpenJDK&…

【Java基础面试三十八】、请介绍Java的异常接口

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;请介绍Java的异常接口 …

【Linux】基本操作指令汇总(不完全)

文章目录 操作系统概念补充lspwdsuechocdtouchmkdirrmdir指令 && rm 指令(重要)man指令cpmvcatmorelessheadtailstat时间相关的指令calfindgrepwcsortuniqwhichziptar:打包/解包&#xff0c;不打开它&#xff0c;直接看内容bcunamectrl cctrl rctrl d\ls cpulsmemdf-hw…

Leetcode 2910. Minimum Number of Groups to Create a Valid Assignment

Leetcode 2910. Minimum Number of Groups to Create a Valid Assignment 1. 解题思路2. 代码实现 题目链接&#xff1a;2910. Minimum Number of Groups to Create a Valid Assignment 1. 解题思路 这一题有点惭愧&#xff0c;我走了弯路&#xff0c;结果居然还是看了大佬们…

Android C/C++ native编程NDK开发中logcat的使用

Android C/C native编程NDK开发中logcat的使用 前言具体用法 前言 在NDK开发过程中&#xff0c;C/C层&#xff0c;需要对代码进行一些调试&#xff0c;日志打印是我们解决异常或崩溃的重要手段&#xff0c;这里我就简单介绍下日志打印三步走。 首先我们先看下官方文档关于日志…

【CSS】gird 网格

网格&#xff08;Grid&#xff09;是一种基于列数的布局系统&#xff0c;它可以帮助开发者创建具有水平和垂直分隔的页面布局。在CSS中&#xff0c;Grid是一种非常强大的布局工具&#xff0c;可以轻松地创建复杂的布局结构。Grid的主要属性包括&#xff1a; grid-template-col…

一天吃透Java面试题

给大家分享我整理的Java高频面试题&#xff0c;有小伙伴靠他拿到字节offer了。 Java基础面试题 Java的特点Java 与 C 的区别JDK/JRE/JVM三者的关系Java程序是编译执行还是解释执行&#xff1f;面向对象和面向过程的区别&#xff1f;面向对象有哪些特性&#xff1f;数组到底是…

npm常用命令与操作篇

npm简介 npm是什么 npm 的英文是&#xff0c;node package manager&#xff0c;是 node 的包管理工具 为什么需要npm 类比建造汽车一样&#xff0c;如果发动机、车身、轮胎、玻璃等等都自己做的话&#xff0c;几十年也做不完。但是如果有不同的厂商&#xff0c;已经帮我们把…

信息检索与数据挖掘 | (五)文档评分、词项权重计算及向量空间模型

目录 &#x1f4da;词项频率及权重计算 &#x1f407;词项频率 &#x1f407;逆文档频率 &#x1f407;tf-idf权重计算 &#x1f4da;向量空间模型 &#x1f407;余弦相似度 &#x1f407;查询向量 &#x1f407;向量相似度计算 &#x1f4da;其他tf-idf权值计算方法 …

Jmeter —— jmeter参数化实现

jmeter参数化 在实际的测试工作中&#xff0c;我们经常需要对多组不同的输入数据&#xff0c;进行同样的测试操作步骤&#xff0c;以验证我们的软件的功能。这种测试方式在业界称为数据驱动测试&#xff0c; 而在实际测试工作中&#xff0c;测试工具中实现不同数据输入的过程称…

数据库管理-第112期 Oracle Exadata 03-网络与ILOM(20231020)

数据库管理-第112期 Oracle Exadata 03-网络与ILOM&#xff08;202301020&#xff09; 在Exadata中&#xff0c;除了对外网络以外&#xff0c;其余网络都是服务于一体机内部各组件的网络&#xff0c;本期对这些网络的具体情况和硬件管理相关做一个讲解。 1 网络分类 1.1 生产…

记调试SMBUS的心得

为什么电池电压读的不对 仔细一看是I2C读取数据的时候少了一个CLK I2C是非常严密的 读数据之后&#xff0c;发送 ACK&#xff0c;让从机准备数据 发送NACK&#xff0c;告诉从机别准备了 ACK和NACK的区别是啥&#xff0c;告诉你&#xff0c;就是NACK先拉高SDA&#xff0c;再…

十四天学会C++之第五天:类的详细讨论

1. 友元函数和友元类 什么是友元函数和友元类&#xff0c;它们的作用。如何声明和使用友元函数和友元类&#xff0c;访问类的私有成员。 友元函数&#xff08;Friend Functions&#xff09; 友元函数是一种特殊的函数&#xff0c;它被允许访问类的私有成员。这意味着即使成员…

为什么要做字节对齐 alignment?

下面这段 C 代码的输出是什么&#xff1f;定义的 Type 占用的字节数&#xff08;下面简称为字节数&#xff09;是多少呢&#xff1f; #include <iostream>struct Type {char a;int b; };int main(void) {std::cout << sizeof(Type) << \n; }经过编译运行&am…

阿里面试(持续更新)

一面&#xff1a; 1 HashMap 实现原理&#xff0c;ConcurrentHashMap 实现原理 HashMap和ConcurrentHashMap都是存储键值对的数据结构&#xff0c;不同的是HashMap是线程不安全的&#xff0c;ConcurrentHashMap是线程安全的&#xff0c;HashMap在高并发情况下会出现数据不一致…

企业知识库软件,快速构建企业知识分享与团队协同的软件

企业知识库是一种特殊的在线协同文档工具&#xff0c;支持包括FAQ、文档、视频、知识图谱等。从本质上讲&#xff0c;它是基于企业知识库软件从而实现内部或外部知识的沉淀、集合、更新、共享等&#xff0c;能为员工或客户提供常见问题的标准回答。 今天我就基于HelpLook &…

敏捷是怎么提高工作效率的

敏捷管理是一门极力减少不必要工作量的艺术。 谷歌、亚马逊、苹果、微信、京东等全球 500 强企业都在用的管理方法&#xff0c;适用于各行各业&#xff0c;被盛赞为应获“管理学的诺贝尔奖”。 它专注于让员工不受种种杂事的羁绊&#xff0c;激发个体斗志&#xff0c;释放出巨大…

阶段六-Day05-MyBatis3

一、多表查询&#xff08;面试题&#xff09; 1. 介绍 多表查询是在企业中必不可少的&#xff0c;无论多么简单的项目里通常会出现多表查询的操作。因为只要是关系型数据库&#xff0c;在设计表时都需要按照范式进行设计&#xff0c;为了减少数据冗余&#xff0c;都会拆成多个…

Proteus仿真--量程自动切换数字电压表(仿真+程序)

本文主要介绍基于51单片机的量程自动切换数字电压表Proteus仿真设计&#xff08;完整仿真源文件及代码见文末链接&#xff09; 简介 硬件电路主要分为单片机主控模块、AD转换模块、量程选择模块以及数码管显示模块 &#xff08;1&#xff09;单片机主控模块&#xff1a;单片…

Golang开发软件

1. 引言 Go&#xff08;也称为Golang&#xff09;是一种开源的编程语言&#xff0c;由Google在2007年启动的项目中开发而来。它是一种静态类型的编译型语言&#xff0c;旨在提供高效、可靠的性能。相比于其他编程语言&#xff0c;Golang具有更高的执行效率和并发能力&#xff…