【C++】特殊类的设计

如何设计一个只能在堆上创建对象的类

思路:封掉正常创建对象的渠道。即私有构造函数,但是不能delete构造函数,因为我还是要创建对象,只是封掉了正常创建对象的渠道。然后在类内正常创建对象,那我就从堆上创建一个返回出去。
但是调用成员函数又需要对象,而我又要创建对象,所以只能用静态成员函数,但是光这样设计还没有封死,如果我在外部用堆上创建的对象拷贝构造一个在栈上的对象也是可行的,解决方法C++98中可以把拷贝构造声明成私有,C++11可以delete拷贝构造。

class OnlyHeap
{
public:
	static OnlyHeap* createObj()
	{
		return new OnlyHeap;
	}
	OnlyHeap(const OnlyHeap& oh) = delete;
private:
	OnlyHeap()
		:_a(0)
	{}

private:
	int _a;
};

这就设计完成了。

如何设计一个只能在栈上创建对象的类

思路:也让构造函数私有,然后自己控制一个接口在栈上创建对象,那我也可以先用接口在栈上创建对象,再用拷贝构造去构造一个在堆上的空间也是可以的,但是这时候我们不能把拷贝构造封掉,因为我的接口在栈上创建的对象还要传值返回,靠的就是拷贝构造,封死创建对象的接口也不能使用了,解决方案就是屏蔽new

class OnlyStack
{
public:
	static OnlyStack CreateObj()
	{
		return OnlyStack();
	}
private:
	OnlyStack()
	:_a(0)
	{}
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;
private:
	int _a;
};

因为一个类可以重载它专属的new和delete,所以我可以把这个类专属的new和delete给屏蔽掉,这样就可以实现。

如何设计一个不能被拷贝的类

思路非常简单,把拷贝构造和赋值运算符重载都封掉即可

如何设计一个不能被继承的类?

C++98中可以把其成员函数都声明成私有,因为派生类的构造和析构等各种函数必须调父类,而父类全部私有,私有在子类是不可见的,以次达到不能被继承的目的
C++11中比较简单,可以直接类后加final关键字,把类设计成最终类,就不可被继承。

如何设计只能创建一个对象的类

设计模式

肯特·贝克和沃德·坎宁安在1987年利用克里斯托佛·亚历山大在建筑设计领域里的思想开发了设计模式并把此思想应用在Smalltalk中的图形用户接口的生成中。一年后Erich Gamma在他的苏黎世大学博士毕业论文中开始尝试把这种思想改写为适用于软件开发。。与此同时James Coplien 在1989年至1991 年也在利用相同的思想致力于C++的开发,而后于1991年发表了他的著作Advanced C++ Idioms,Erich Gamma 得到了博士学位,然后去了美国,在那与Richard Helm, Ralph Johnson ,John Vlissides合作出版了Design Patterns - Elements of Reusable Object-Oriented Software 一书,里面的内容就是最初的23种设计模式。这四位作者在软件开发领域里也以他们的匿名著称Gang of Four(四人帮,简称GoF)。

所谓设计模式(Design Pattern)就是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。可以理解为一些写代码的套路。
设计模式其实有很多种,但是很多都不常用,比较常用的比如有:

迭代器模式
用来访问一些数据结构比如:数组,链表,二叉树,哈希表等等

当要访问和修改这些数据结构时,如果暴露出底层结构,会有以下问题:
1.访问不方便,需要熟悉底层的结构
2.暴露底层结构,别人可以直接访问和修改数据,不便于管理。

如果使用迭代器模式,用迭代器访问和修改数据:
1.以统一的方式封装访问结构,底层结构不暴露
2.可以使用统一的方式去轻松访问容器。不关心底层是树,还是链表的
所以迭代器模式在保证封装的情况下提供了一种统一的方式让我们访问容器,是一种非常棒的设计。

范围for也是在迭代器的基础上实现的,所以上层用的越轻松,底层就封装了越多的细节,所以语言越高级,藏在冰山下面的东西是越多的。

适配器模式
栈,队列,优先级队列,反向迭代器等这些数据结构如果要让我们自己从头开始实现一个,那是非常麻烦的。那可以用别的简单的结构封装出来一个,这就是适配器模式。

还有一个比较重要并且是使用最多的模式之一——单例模式

单例模式

假设现在我有一个需求是要有一个全局变量,并且要保证这个变量在全局只有一个,我有一个函数会对该变量进行++操作,主函数和操作函数不在一个cpp文件中,如何设计这个变量呢?

首先,如果我在头文件直接定义全局变量,会存在编译报错的问题,因为一个在.h文件中定义的变量,如果这个.h文件在两个.cpp文件中展开,就会出现重定义的问题。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这是在两个文件重复包含的问题,也不能用#pragma once,因为其解决的是在同一个文件重复包含的问题,条件编译也一样。

那只能用static全局变量,普通的全局变量在多个文件都可见,而static会影响函数和变量的连接属性,使其只在当前文件可见。
在这里插入图片描述
但是静态的全局变量只在当前文件可见。是可以调用了,但是又导致出现了两个变量,地址都不一样。那怎么对其操作?

在这里插入图片描述
解决方法是:变量的声明和定义分离

我在头文件只声明变量,然后在操作的地方定义,结果就是在编译时只有要对其操作的文件才有该变量,main函数里没有,但是main函数有该变量的声明,可以通过编译,在链接时,才会找到该变量,这时候操作也完成了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如果我有好几个变量或数据结构需要被设计成这种特性,除了像刚刚那样分离声明定义,也可以将其设计成单例模式。

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

任意一个类都可以设计成单例模式

如何设计?
类里面的成员天生就是声明,所以把信息设计成类的成员变量后不用加extern,并且我不想让别人可以随便创建,所以我可以把构造函数私有,然后自定义一个GetInstance用来获取对象,如何获取对象?有两种方式:懒汉模式,饿汉模式

他们的区别是:
饿汉:在main函数之前就创建好了对象,main函数之后程序随时可以访问这个单例对象。
懒汉:事先没有准备,第一次访问时才创建单例对象。

饿汉模式

类内声明一个静态的自己类的对象,然后在需要操作的cpp文件里面定义,GetInstance每次调用的都是这个对象
为了防止恶意拷贝可以把拷贝构造封死

//头文件
class CallInfo
{
public:
	static CallInfo& GetInstance()
	{
		return _inst;//每次都调用一个静态对象
	}
	void AddCnt(int n=1)
	{
		_cnt += n;
	}
	void PushV(const int n)
	{
		_v.push_back(n);
	}
	int GetCnt()
	{
		return _cnt;
	}
	CallInfo(const CallInfo&) = delete;
private:
	CallInfo()
		:_cnt(0)
	{}
private:
	int _cnt;//不需要extern,因为类里面的对象本来就是声明,对象才是定义
	vector<int> _v;
	static CallInfo _inst;//定义一个自己类的对象
};
void Func();
//操作文件
CallInfo CallInfo::_inst;//定义该静态对象
void Func()
{
	
	for (int i = 0; i < 100; i++)
	{
		CallInfo::GetInstance().AddCnt();
		CallInfo::GetInstance().PushV(i);
	}
}

主要方式
构造函数私有化,提供一个获取对象的方式,但是这个方式是每次去获取之前创建好的那个对象(类里的那个静态对象,要在操作的cpp文件定义)。封死拷贝。

懒汉模式

用static的类指针,初始化为nullptr,获得对象的时候判断一下,如果是空我再创建,然后返回指针引用,第一次调用时指针是空,会创建一个对象,之后调用就不再是空,会直接返回该对象。

为什么要用静态:因为每次获取都要确保获取就是那一个,只有静态才能保证全局唯一。、

在这里插入图片描述
定义:
在这里插入图片描述

这样实现在多线程情况下会有线程安全问题:
假设我有两个线程t1,t2。t1先new了对象,但是刚好被切走了,t2上来new完一顿加,t1回来把自己new好的对象返回,加自己的,t2再回来那之前加的都没了,这就造成了数据丢失,同时还会有内存泄漏的问题,因为之前t2new的对象并没有被释放,它是直接被替换的。解决的方法就是加锁。
引入一个静态的锁,和被访问信息一样在类外定义,在获得对象时,先上锁
在这里插入图片描述
这样设计还是有一点问题:加锁是为了保护第一次获取对象,只要对象创建好了以后,就没有线程安全问题了,如果这样写后面每次获取对象都要加锁,虽然不会影响正确性,但是会影响效率!!
那我也不能把锁加在if里面,如果锁被加在里面,假设两个线程有一个先拿到锁,还有一个也进到了if里面但是没拿到锁,会等一会,先进去的new好对象,操作完解锁,后进来的从if下面继续运行,相当于没加锁。

所以只能双检查加锁
在这里插入图片描述

还有一个资源释放的问题
一般情况下单例对象不用释放,也不用担心泄漏问题,因为只要程序正常结束,资源都会自动还给系统,并且这个对象只有一个,系统自动回收也没什么问题。但是如果我有一些特殊的需求,需要在释放资源时进行一些别的操作,那可以自己主动释放
释放方法可以时提供主动调用的接口,但是这种方法不太常见,比较常见的方法是提供一个内部类进行回收

内嵌垃圾回收类的使用方法
和创建单例对象一样,先在类内声明一个静态的回收类对象,然后再类外定义,而该类只有一个成员函数就是析构函数,析构函数额功能是释放单例类对象,有了回收类对象,在离开作用域时就可以调用回收类的析构函数实现回收。
以下是懒汉模式的单例类实现代码

//头文件
class CallInfo
{
public:
	static CallInfo& GetInstance()
	{
		
		if (_pinst == nullptr) {//保证线程安全
			std::unique_lock<mutex> lock(_mtx);
			if(_pinst==nullptr)//保证单例
				_pinst = new CallInfo;
		}
		return *_pinst;
	}
	// 实现一个内嵌垃圾回收类 
	class CGarbo {
	public:
		~CGarbo() {
			if (_pinst) {
				delete _pinst;
				_pinst = nullptr;
			}
		}
	};
	void AddCnt(int n = 1)
	{
		_cnt += n;
	}
	void PushV(const int n)
	{
		_v.push_back(n);
	}
	int GetCnt()
	{
		return _cnt;
	}
	CallInfo(const CallInfo&) = delete;
private:
	CallInfo()
		:_cnt(0)
	{}
private:
	int _cnt;
	vector<int> _v;
	static CallInfo* _pinst;//定义一个自己类的对象,因为时静态的(全局的,数据这个类,相当于一个全局对象,只不过被限制在类域里)
	static mutex _mtx;
	static CGarbo gc;
};
void Func();
//操作文件
CallInfo* CallInfo::_pinst(nullptr);
mutex CallInfo::_mtx;
CallInfo::CGarbo gc;

还有一种比较简单的实现方式:
不用对象指针,锁,直接在获取对象的函数里定义一个局部的静态对象,然后返回它,而局部静态对象的生命周期是全局,作用域只有当前函数域,这样只有我第一次调用该函数会创建对象,之后就会直接返回该对象。

class CallInfo
{
public:

	static CallInfo& GetInstance()
	{

		static CallInfo inst;//创建局部静态对象,生命周期属于全局,但是作用域只在当前函数
		return inst;//第一次调用时会初始化
	}
	void AddCnt(int n = 1)
	{
		_cnt += n;
	}
	void PushV(const int n)
	{
		_v.push_back(n);
	}
	int GetCnt()
	{
		return _cnt;
	}
	CallInfo(const CallInfo&) = delete;
private:
	CallInfo()
		:_cnt(0)
	{}
private:
	int _cnt;
	vector<int> _v;
};
void Func();

这种实现方式在C++98的多线程调用时,静态局部对象的构造初始化不能保证线程安全问题,但是C++11优化了这个问题,让静态局部对象的构造初始化能够做到线程安全,所以这种实现方法只能在C++11中使用。

懒汉饿汉的区别

两个单例类A,B要求A先创建,B后创建,B的创建依赖A,,

饿汉模式
优点:
比较简单,并且没有线程安全问题(因为我的对象是在之前就创建好的)。
缺点:
1.饿汉无法控制单例创建初始化的顺序
2.如果单例对象的初始化很费时,会导致程序启动慢,像卡死了一样

懒汉模式:
可以对应解决饿汉二点两个缺点。
优点:
1.可以在定义时调整定义的顺序。
2.没有启动的要求,只初始化一个指针,GetInstance才初始化对象。
缺点:
相对复杂,尤其是还要控制线程安全问题。

以上就是本篇的全部内容

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

c铁柱同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值