【C++】特殊类实现

news2024/11/17 23:43:10

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

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

  • 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 new
	void* 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.coloradmin.cn/o/1118506.html

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

相关文章

聚类分析 | Python密度聚类(DBSCAN)

密度聚类是一种无需预先指定聚类数量的聚类方法&#xff0c;它依赖于数据点之间的密度关系来自动识别聚类结构。 本文中&#xff0c;演示如何使用密度聚类算法&#xff0c;具体是DBSCAN&#xff08;Density-Based Spatial Clustering of Applications with Noise&#xff09;来…

磁盘清理 | 已经卸载的软件还出现在应用和功能里怎么办?

一句话总结解决方法&#xff1a; 安装Geek Uninstaller,删除卸载残留。 问题描述&#xff1a; 最近磁盘满了&#xff0c;需要删除一些平时不常用的软件&#xff0c;但是发现一个问题。就是已经删除的软件&#xff0c;仍然会出现在“应用与功能”中。并且显示卸载图标为灰色&am…

基于PHP的线上购物商城,MySQL数据库,PHPstudy,原生PHP,前台用户+后台管理,完美运行,有一万五千字论文。

目录 演示视频 基本介绍 论文截图 功能结构 系统截图 演示视频 基本介绍 基于PHP的线上购物商城&#xff0c;MySQL数据库&#xff0c;PHPstudy&#xff0c;原生PHP&#xff0c;前台用户后台管理&#xff0c;完美运行&#xff0c;有一万五千字论文。 现如今,购物网站是商业…

用Python进行websocket接口测试

这篇文章主要介绍了用Python进行websocket接口测试&#xff0c;帮助大家更好的理解和使用python&#xff0c;感兴趣的朋友可以了解下 我们在做接口测试时&#xff0c;除了常见的http接口&#xff0c;还有一种比较多见&#xff0c;就是socket接口&#xff0c;今天讲解下怎么用P…

(一)docker:建立oracle数据库

前言&#xff0c;整个安装过程主要根据docker-images/OracleDatabase/SingleInstance /README.md &#xff0c;里边对如何制作容器讲的比较清楚&#xff0c;唯一问题就是都是英文&#xff0c;可以使用谷歌浏览器自动翻译成中文&#xff0c;自己再对照英文相互参照来制作提前准备…

P1 缓冲池管理

文章目录 Task1 LRU-K 替换策略Task2 缓冲池管理Task3 读/写页面保护 Task1 LRU-K 替换策略 LRU-K算法&#xff1a;当访问次数达到K次后&#xff0c;将数据索引从历史队列移到缓存队列中&#xff08;缓存队列时间降序&#xff09;&#xff1b;缓存数据队列中被访问后重新排序&…

Python--循环中的两大关键词 break 与 continue

在Python循环中&#xff0c;经常会遇到两个常见的关键词&#xff1a;break 与 continue break&#xff1a;代表终止整个循环结构 continue&#xff1a;代表中止当前本次循环&#xff0c;继续下一次循环 break&#xff1a; 英 /breɪk/ v. 打破&#xff0c;打碎&#xff0c…

c语言练习95:练习使用双向链表(实现增删改查)

练习使用双向链表&#xff08;实现增删改查&#xff09; 是指针指向了一块被释放的空间 解决方案&#xff1a; plistNULL List.h #pragma once #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<assert.h> #include<…

java实现多线程下载器

前言&#xff1a; &#x1f44f;作者简介&#xff1a;我是笑霸final&#xff0c;一名热爱技术的在校学生。 &#x1f4dd;个人主页&#xff1a;个人主页1 || 笑霸final的主页2 &#x1f4d5;系列专栏&#xff1a;项目专栏 &#x1f4e7;如果文章知识点有错误的地方&#xff0c;…

2316. 统计无向图中无法互相到达点对数(leetcode)并查集-------------------Java实现

2316. 统计无向图中无法互相到达点对数&#xff08;leetcode&#xff09;并查集-------------------Java实现 题目表述 给你一个整数 n &#xff0c;表示一张 无向图 中有 n 个节点&#xff0c;编号为 0 到 n - 1 。同时给你一个二维整数数组 edges &#xff0c;其中 edges[i…

使用IDEA时遇到java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver报错的解决方案

目录 一、项目环境二、可能原因解决方案1. 没有导入mysql的jar包2. mysql的jar包版本问题 一、项目环境 二、可能原因解决方案 1. 没有导入mysql的jar包 先检查项目lib文件夹下有没有mysql的jar包&#xff0c;没有就把jar包复制到该目录下 再检查项目结构中有没有导入mysql…

使用vscode搭建虚拟机

首先vscode插件安装 名称: Remote - SSH ID: ms-vscode-remote.remote-ssh 说明: Open any folder on a remote machine using SSH and take advantage of VS Codes full feature set. 版本: 0.51.0 VS Marketplace 链接: https://marketplace.visualstudio.com/items?it…

掌握 C++ 中 static 关键字的多种使用场景

static是什么 在最开始C中引入了static关键字可以用于修饰变量和函数&#xff0c;后来由于C引入了class的概念&#xff0c;现在static可以修饰的对象分为以下5种&#xff1a; 成员变量&#xff0c;成员函数&#xff0c;普通函数&#xff0c;局部变量&#xff0c; 全局变量 s…

vmware安装 Rocky9(自定义分区安装)

一、下载镜像 访问官网&#xff0c;下载dvd的镜像 Download Rocky | Rocky Linuxhttps://rockylinux.org/download 二、新建vmware虚拟机 1、vmware尽量选择vmware17 2、下一步 3、稍后安装 4、选择系统类型&#xff1a;red hat9 5、自定义安装位置 6、根据电脑配置&#…

动画系统的前世今生(一)

掐指一算&#xff0c;五年没更新过我的CSDN账号啦&#xff0c;方向也从人工智能变成了计算机图形学&#xff0c;当然也依旧会关注AI的发展&#xff0c;之前在知乎上写了一些文章[传送门]&#xff0c;后续也会逐渐同步到CSDN上&#xff5e; 这个系列将包含五篇文章&#xff0c;内…

allegro中shape的一些基本操作(二)——铜皮添加网络、合并shape

给铜皮&#xff08;shape&#xff09;添加网络 例如下图&#xff0c;想要给这个新添加的shape添加到GND的网络&#xff0c;可以先选中这个shape&#xff0c;让其进入shape编辑模式&#xff0c;然后再右键点击&#xff0c;最后再PCB上点击GND网络 选中铜皮后在铜皮上右键&…

字典树学习笔记

trie 树&#xff0c;即字典树&#xff0c;是一种可以实现 O ( S ) O(S) O(S) 的预处理&#xff08; S S S 为所有字符串的长度和&#xff09;&#xff0c; O ( N ) O(N) O(N)&#xff08; N N N 为查询的字符串的长度&#xff09;的查询的数据结构。 举个栗子&#xff0c;对于…

Rust-后端服务调试入坑记

这篇文章收录于Rust 实战专栏。这个专栏中的相关代码来自于我开发的笔记系统。它启动于是2023年的9月14日。相关技术栈目前包括&#xff1a;Rust&#xff0c;Javascript。关注我&#xff0c;我会通过这个项目的开发给大家带来相关实战技术的分享。 如果你关注过我的Rust 实战里…

Baize_h1mini六足机器人零件准备

导航在这里&#xff1a; Baize_H1mini六足机器人制作教程&#xff08;开源&#xff09;_ros 六足机器人教程-CSDN博客 你现在在地图的红色字体位置&#xff08;走到终点就制作完成了&#xff09;&#xff1a; 重要提示&#xff1a;自己使用打印机打印零件时&#xff0c;对于新…

Pandas数据处理分析系列5-数据如何提取

Pandas-数据提取 ①通过索引提取数据 # 提取前10行数据 df.head(10) # 提取末尾10行数据 df.tail(10) # 通过列名提取数据 df[列名’] # 通过布尔条件提取数据 df[df[列名] > 10] # 多条件过滤 df[(df[列名1] > 10) & (df[列名2] < 20)] df1=pd.read_excel(&quo…