【C++】特殊类的设计

2023-09-17 17:43:00


1. 设计一个类, 不能被拷贝

💕 C++98方式:

在C++11之前,想要一个一个类不被拷贝,只有将拷贝构造函数定义为私有,这样在类外就不能调用拷贝构造函数来构造对象了。但是在类内还是可以调用拷贝构造函数来构造对象。

在这里插入图片描述

所以正确的做法是 将拷贝构造函数定义为私有,同时拷贝构造函数只声明,不实现。这样即使在类中掉哦那个了拷贝构造函数,编译器也会将错误检查出来。

class CopyBan
{
public:
	CopyBan()
	{
		_ptr = new char[10]{ 0 };
	}
	~CopyBan()
	{
		delete[] _ptr;
	}

	void func()
	{
		CopyBan tmp(*this);
	}
	
private:
	// 重写深拷贝构造函数
	CopyBan(const CopyBan& cb);
	char* _ptr;
};

在这里插入图片描述


💕 C++11方式:

C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。 同时,这种方法也不再需要将拷贝构造函数定义为私有。

class CopyBan
{
public:
	CopyBan()
	{
		_ptr = new char[10]{ 0 };
	}
	~CopyBan()
	{
		delete[] _ptr;
	}

	CopyBan(const CopyBan& cb) = delete;

private:
	char* _ptr;
};

在这里插入图片描述


2. 设计一个类, 不能被继承

💕 C++98方式:

C++98中构造函数私有化,派生类中调不到基类的构造函数,则无法调用父类的构造函数完成父类成员的初始化工作,从而达到父类不能被继承的效果。

class NonInherit
{
public:
	static NonInherit GetInstance()
	{
		return NonInherit();
	}
private:
	NonInherit()
	{}
};
class subClass : public NonInherit
{
public:
	subClass()
	{}
private:
	int _a = 0;
};

在这里插入图片描述


💕 C++11方式:

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

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

在这里插入图片描述


3. 设计一个类, 只能在堆上创建对象

一般的类可以在三个不同的存储位置创建对象:

  • 在栈上创建对象,对象出了局部作用域自动销毁
  • 通过new关键字在堆上创建对象,对象出了局部作用域不会自动销毁,需要我们手动销毁。否则则会发生内存泄漏
  • 通过static关键字在静态区创建对象,对象的作用域为定义时所在的局部域,而对象的生命周期伴随着整个进程,这个对象在mian调用结束后由操作系统自动回收

💕 方法一:构造函数私有化

将构造函数声明为私有,同时删除拷贝构造函数,然后提供一个静态成员函数,在该静态成员函数中完成堆对象的创建。

class HeapOnly
{
public:
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
	HeapOnly(const HeapOnly& ho) = delete;
private:
	HeapOnly()
	{}
};

静态成员没有this指针,所以可以通过类名+域作用限定符来进行调用而不需要通过对象调用。同时我们还需要删除拷贝构造函数,防止在类外通过下面这种取巧的方式来创建栈区或者堆区对象:

HeapOnly* pho1 = HeapOnly::CreateObj();
HeapOnly pho2(*pho1); // 通过拷贝构造函数在栈上创建对象
static HeapOnly ho3(*pho1); // 通过拷贝构造函数在静态区上创建对象

💕 方法二:析构函数私有化

将析构函数私有化,同时提供一个专门的成员函数,在该成员函数中完成堆对象的析构

class HeapOnly
{
public:
	HeapOnly()
	{}

	void Destroy()
	{
		this->~HeapOnly();
	}
private:
	~HeapOnly()
	{}
};

对于在堆上创建的对象,编译器并不会主动调用析构函数来回收资源,而是由用户手动进行delete或者进程退出后由操作系统回收,所以编译器并不会报错。但对于自定义类型的对象,delete会首先调用其析构函数完成兑现资源的清理,然后再调用operator delete 释放对象的空间,所以这里我们不能使用delete关键字来手动释放new出来的对象,因为调用析构函数会失败。

所以我们需要一个Destroy成员函数,通过它来调用析构函数完成资源的清理。这个Destroy函数不需要声明为静态类型,因为只有类的对象才需要调用它。最后,我们也不需要再删除拷贝构造函数了,因为拷贝出来的栈对象或者静态对象压根儿无法创建出来。

在这里插入图片描述


3. 设计一个类, 只能在栈上创建对象

💕 方法一:在类中禁用operator new 和 operator delete函数

new和delete是C++中的关键字,其底层通过调用operator new 和operator delete函数来开辟和释放空间。

operator new 和 operator delete 函数是普通的全局函数,而并非运算符重载,它们的函数名就长这样罢了。因此,我们可以在类中重载operator new和 operator delete 函数,然后将他们声明为删除函数,这样就不能通过new和delete再堆上创建对象了,但是我们仍然可以在静态区创建对象,与类的要求不符。

class StackOnly
{
public:
	StackOnly(int x = 0)
		:_x(x)
	{}
	~StackOnly()
	{}
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;
private:
	int _x;
};

在这里插入图片描述


💕 方法二:构造函数私有化,提供一个在栈上创建对象的静态成员函数

这种方式和设计一个 只能在堆上创建对象的类的思路是一样的。但是不能删除拷贝构造函数,否则就不能通过下面这种方式构造栈对象了。

StackOnly st = StackOnly::CreateObj();

但是不禁用拷贝构造函数又会导致可以通过拷贝构造函数创建出静态区上的对象,所以我们设计出的只能在栈上创建对象的类是有缺陷的。

class StackOnly
{
public:
	static StackOnly CreateObj(int x)
	{
		return StackOnly(x);
	}
private:
	StackOnly(int x = 0)
		:_x(x)
	{}
	int _x;
};

int main()
{
	StackOnly st1 = StackOnly::CreateObj(1);

	return 0;
}

在这里插入图片描述


4. 创建一个类, 只能创建一个对象(单例模式)

设计模式(Design Pattern) 是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。

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

单例模式:

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

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


饿汉模式

饿汉模式是将构造函数私有,然后删除拷贝构造函数和赋值运算符重载函数,由于单例模式全局只允许有一个唯一对象,所以我们可以定义一个静态类对象作为类的承运,然后提供一个GetInstance接口来获取这个静态类对象。静态类对象需要在类内声明,类外定义,定义时需要指定类域。同时,GetInstance接口也必须是静态函数。

饿汉模式的特点是在类加载的时候就创建单例对象,因此其实梨花在程序运行之前(main函数调用之前就已经完成)实现如下:

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		return _ins;
	}

	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton& s) = delete;

private:
	// 限制在类外面随意创建对象
	Singleton()
	{}
	
	static Singleton* _ins;
};

Singleton* Singleton::_ins = new Singleton;

因为饿汉模式在main函数前就被创建,所以它不存在线程安全问题,但是它也存在一些缺点

  • 有的单例对象构造十分耗时或者需要占用很多资源,比如加载插件、 初始化网络连接、读取文件等等,会导致程序启动时加载速度慢。
  • 饿汉模式在程序启动时就创建了单例对象,所以即使在程序运行期间并没有用到该对象,它也会一直存在于内存中,浪费了一定的系统资源。
  • 多个单例类存在初始化依赖关系时,饿汉模式无法控制。比如A、B两个单例类存在于不同的文件中,我们要求先初始化A,再初始化B,但是A、B谁先启动初始化是由OS自动进行调度控制的,我们无法进行控制。

多线程模式下的饿汉模式:

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		return _ins;
	}
	void Add(const string& str)
	{
		_mtx.lock();
		_v.push_back(str);
		_mtx.unlock();
	}
	void Print()
	{
		_mtx.lock();
		for (auto& e : _v)
			cout << e << endl;
		cout << endl;
		_mtx.unlock();
	}

	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton& s) = delete;

private:
	// 限制在类外面随意创建对象
	Singleton()
	{}
	vector<string> _v;
	static Singleton* _ins;
	mutex _mtx;
};

Singleton* Singleton::_ins = new Singleton;

int main()
{
	srand(time(0));

	int n = 100;
	thread t1([n]() {
		for (size_t i = 0; i < n; ++i)
		{
			Singleton::GetInstance()->Add("t1线程:" + to_string(rand()));
		}
	});

	thread t2([n]() {
		for (size_t i = 0; i < n; ++i)
		{
			Singleton::GetInstance()->Add("t2线程:" + to_string(rand()));
		}
	});

	t1.join();
	t2.join();
	Singleton::GetInstance()->Print();
	return 0;
}

在这里插入图片描述


懒汉模式

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

懒汉模式 是另一种实现单例模式的方式,与饿汉模式不同的是:懒汉模式是延迟示例化的,即在第一次访问时才创建唯一的实例。

懒汉模式的实现思路是将构造函数私有化,然后提供一个静态私有指针成员来保存唯一实例的地址,并通过一个公共的静态方法来获取该实例。

class Singleton
{
public:
	static Singleton& GetInstance()
	{
		// 双检查加锁
		if (_ins == nullptr)
		{
			_smtx.lock();
			if(_ins == nullptr)
				_ins = new Singleton;
			_smtx.unlock();
		}
		return *_ins;
	}

	static void DelInstance()
	{
		_smtx.lock();
		if (_ins) {
			delete _ins;
			_ins = nullptr;
		}
		_smtx.unlock();
	}

	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton& s) = delete;

	~Singleton()
	{
		// 持久化
		// 比如要求程序结束时,将数据写到文件,单例对象析构时持久化就比较好
	}
private:
	// 限制在类外面随意创建对象
	Singleton()
	{}
	static Singleton* _ins;
	static mutex _smtx;
};

Singleton* Singleton::_ins = nullptr;
mutex Singleton::_smtx;

懒汉模式下的线程安全问题以及双检查加锁

懒汉模式也引入了新的问题:单例对象的创建线程是不安全的。对于懒汉模式来说,由于其单例对象是在第一次使用时才创建的,那么在多线程模式下,就有可能会存在多个线程并行/并发的去执行 _psins = new Singleton 语句,从而导致前面创建出来单例对象指针被后面的覆盖,最终发生内存泄露。

在这里插入图片描述

单例对象的资源释放与保存问题

一般来说单例对象都是不需要考虑释放的,因为不管是饿汉模式还是懒汉模式,单例对象都是全局的,全局资源在程序结束后会被自动回收 (进程退出后OS会解除进程地址空间与物理内存的映射)。但是我们也可以手动对其进行回收。需要注意的是,有时我们需要在回收资源之前将资源的相关数据保存到文件中,这种情况下我们就必须手动回收了。

  1. 在类中定义一个静态的 DelInstance接口,来回收与保存资源。
static void DelInstance()
{
	_smtx.lock();
	if (_ins) {
		delete _ins;
		_ins = nullptr;
	}
	_smtx.unlock();
}
  1. 定义一个内部的GC类,通过Singleton类中的一个静态GC类对象,使得程序在结束回收GC对象时自动调用GC类的析构从而完成资源回收与数据保存工作。避免我们忘记调用Dellnstance接口而丢失数据。

例如:

class Singleton
{
public:
	static Singleton& GetInstance()
	{
		if (_ins == nullptr)
		{
			// 双检查加锁
			_smtx.lock();
			if(_ins == nullptr)
				_ins = new Singleton;
			_smtx.unlock();
		}
		return *_ins;
	}

	void Add(const string& str)
	{
		_mtx.lock();
		_v.push_back(str);
		_mtx.unlock();
	}

	void Print()
	{
		_mtx.lock();
		for (auto& e : _v)
			cout << e << endl;
		cout << endl;
		_mtx.unlock();
	}

	static void DelInstance()
	{
		_smtx.lock();
		if (_ins) {
			delete _ins;
			_ins = nullptr;
		}
		_smtx.unlock();
	}

	// 单例对象回收
	class GC
	{
	public:
		~GC()
		{
			DelInstance();
		}
	};
	
	static GC _gc;

	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton& s) = delete;

	~Singleton()
	{
		// 持久化
		// 比如要求程序结束时,将数据写到文件,单例对象析构时持久化就比较好
	}
private:
	// 限制在类外面随意创建对象
	Singleton()
	{}
	vector<string> _v;
	static Singleton* _ins;
	mutex _mtx;
	static mutex _smtx;
};

Singleton* Singleton::_ins = nullptr;
mutex Singleton::_smtx;
Singleton::GC Singleton::_gc;

在这里插入图片描述


懒汉模式的一种简单实现方式

class Singleton
{
public:
	static Singleton& GetInstance()
	{
		static Singleton sins;
		return sins;
	}
	Singleton(const Singleton& sin) = delete;
	Singleton& operator=(const Singleton& sin) = delete;
private:
	Singleton()
	{}
};

上面这种实现方式的缺点就是不稳定,因为只有在 C++11 及其之后的标准中局部静态对象的初始化才是线程安全的,而在 C++11 之前的版本中并不能保证。

更多推荐

Oracle系列十九:Oracle的体系结构

Oracle的体系结构1.物理结构2.内存结构2.1SGA2.2后台进程3.逻辑结构1.物理结构Oracle数据库的物理结构由参数文件、控制文件、数据文件和日志文件组成,用于存储和管理数据库的数据和元数据,每个文件都扮演着不可或缺的角色。参数文件用于配置数据库的初始化参数控制文件记录数据库的结构和状态信息数据文件存储了

Docker赋能物联网:探索软件供应链的优势、挑战和安全性

作者:JFrog大中华区总经理董任远随着联网设备硬件性能的日益提升及价格愈发低廉,物联网应用的复杂性随之提升。常用的容器化平台Docker能够帮助精简流程,助力开发人员更轻松地创建和维护物联网应用。本文将探讨Docker为物联网开发带来的优势,部署和维护应用程序时需考虑的挑战,以及如何将安全最佳实践应用于物联网。Doc

目前最流行的无人机摄影测量软件有哪些?各有什么特点?

文章目录1.Pix4Dmapper2.PhotoScan3.ContextCapture4.天工GodWork5.TrimbleInpho6.IMAGINEPhotogrammetry7.大疆智图推荐阅读:《无人机航空摄影测量精品教程》包括:无人机航测外业作业流程(像控点布设、航线规划、仿地飞行、航拍)和内业数据处理软

PDCA循环

目录1.认识PDCA:2.PDCA循环的经典案例3.PDCA的四个阶段和八个步骤4.PDCA循环的优缺点:5.案例6.其他作用1.认识PDCA:PDCA循环最早由美国质量统计控制之父Shewhat(休哈特)提出的PDS(PlanDoSee)演化而来,由美国质量管理专家戴明改进成为PDCA模式,所以又称为“戴明环”。PD

机器人中的数值优化(十七)—— 锥与对称锥

本系列文章主要是我在学习《数值优化》过程中的一些笔记和相关思考,主要的学习资料是深蓝学院的课程《机器人中的数值优化》和高立编著的《数值最优化方法》等,本系列文章篇数较多,不定期更新,上半部分介绍无约束优化,下半部分介绍带约束的优化,中间会穿插一些路径规划方面的应用实例二十八、锥与对称锥1、尖锥锥是一种特殊的集合,当满足

【2023年11月第四版教材】第14章《沟通管理》(第一部分)

第14章《沟通管理》(第一部分)1章节说明2管理基础2.1沟通具体形式包括2.2沟通模型:★★★(17下41)(18下43)2.3沟通模型包含5种状态2.4沟通分类3管理过程3.1管理的过程★★★(21上42)(22上43)⑵下42)(22下43)(22下案例)3.2管理ITTO汇总★★★1章节说明【本章分值预测】大部

IP地址与代理IP:了解它们的基本概念和用途

在互联网世界中,IP地址和代理IP是两个常见但不同的概念,它们在网络通信、隐私保护和安全方面发挥着重要作用。本文将介绍什么是IP地址和代理IP,以及它们在网络中的作用和应用。IP地址是什么?IP地址,全称为InternetProtocolAddress,是互联网上设备的唯一标识符。它类似于房屋地址,帮助数据包找到它们需

软件测试用例经典方法 | 单元测试法案例

单元测试又称模块测试,是对软件设计的最小单元的功能、性能、接口和设计约束等的正确性进行检验,检查程序在语法、格式和逻辑上的错误,并验证程序是否符合规范,以发现单元内部可能存在的各种缺陷。单元测试的对象是软件设计的最小单位——模块、函数或者类。在传统的结构化程序设计语言(如C语言)中,单元测试的对象一般是函数或者过程。在

python文件(概念、基本操作、常用操作、文本文件的编码方式)

嗨喽,大家好呀~这里是爱看美女的茜茜呐👇👇👇更多精彩机密、教程,尽在下方,赶紧点击了解吧~python源码、视频教程、插件安装教程、资料我都准备好了,直接在文末名片自取就可1.文件的概念1.1文件的概念和作用计算机的文件,就是存储在某种长期储存设备上的一段数据长期存储设备包括:硬盘、U盘、移动硬盘、光盘…文件的作

清水模板是什么材质?

清水模板是建筑施工中常用的一种模板,用于浇筑混凝土结构的形成和支撑。它是指没有进行任何装饰和涂层处理的模板,通常由木材制成,如胶合板、钢模板等。下面是关于清水模板的详细介绍。清水模板的材质多样,其中最常见的是胶合板。胶合板是由多层薄木板通过交错堆叠、胶合而成的板材。由于其具有较高的强度、稳定性和耐久性,因此在建筑施工中

Java-根据模板生成PDF

文章目录前言一、准备模板二、代码实现三、源代码总结前言在有些场景下我们可能需要根据指定的模板来生成PDF,比如说合同、收据、发票等等。因为PDF是不可编辑的,所以用代码直接对PDF文件进行修改是很不方便的,这里我是通过itext和AdobeAcrobat来实现的,以下就是具体实现方法。一、准备模板AdobeAcroba

热文推荐