特殊类设计

文章介绍了如何在C++中设计特殊类,包括不能拷贝的类、只能在堆上创建的对象、只能在栈上创建的对象以及不能被继承的类。此外,详细讨论了单例模式的饿汉模式和懒汉模式,包括它们的实现、优缺点以及线程安全的处理。最后提到了对象的回收和防止重复调用回收函数的策略。
摘要由CSDN通过智能技术生成

特殊类设计

简单的特殊类设计

设计一个不能拷贝的类

这个还是挺简单的,在C++98和C++11中有两种不同的设计方法,在C++98中可以将构造函数封装成私有成员,在C++11中可以在构造函数后面加 =delete,让编译器删除该默认成员函数

代码也很简单:

// C++98
class CopyForbid
{

private:
	CopyForbid(const CopyForbid& cb);
	CopyForbid& operator= (const CopyForbid& cb);
};
// C++11
class CopyForbid11
{

private:
	CopyForbid(const CopyForbid& cb)=delete;
	CopyForbid& operator= (const CopyForbid& cb)=delete;
};

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

设计一个只能在堆上创建对象的类,首先要知道,这个类的构造函数不能再放在public中,所以首先将构造函数私有,在public中通过调用函数从而创建对象,但是这里又出现的一个问题是:没有对象如何调用对象的成员函数呢?所以这里就要将这个函数用static修饰为静态函数。于此同时还要把拷贝构造函数封掉,否则的话还是可以通过拷贝构造函数构造一个栈上的对象

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

另外还有一种方法是将析构函数写进private,但是回收对象需要调用一个public成员函数进行对对象的回收

class HeapONly
{
public:
	HeapONly()
	{}
	void Destory()
	{
		this->~HeapONly();
	}
private:
	~HeapONly()
	{}
};

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

一样的思维,既然对创建这个类有限制,那么首先要做的就是将这个类的构造函数设置为private,其次写一个公有静态成员函数函数进行对象的创建,当然也要记得讲拷贝构造函数封掉,否则的话可能会导致赋值给一个静态区的变量。代码如下:

class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		return StackOnly();
	}
private:
	StackOnly()
	{}
    StackOnly(const StackOnly&) = delete;
};

设计一个不能被继承的类

在C++98中,如果想要设计一个不能被子类继承的类,只能将父类的构造函数设置为私有,子类找不到父类的构造函数,就无法继承。而在C++11中可以使用final关键字。

// 设计一个类,不能被继承
class NoInherit
{
public:
	static NoInherit CreateProject()
	{
		return NoInherit();
	}
private:
	NoInherit()
	{}
};  // C++98
class A final
{
    // ...
}; // C++11

单例模式

饿汉模式

单例模式是应用最广的设计模式之一,也是程序员应该很熟悉的一个设计模式,使用单例模式必须保证类只能创建一个对象。

那为什么会有单例模式呢?

在开发过程中,很多时候一个类我们希望它只创建一个对象,比如:线程池、缓存、网络请求等。当这类对象有多个实例时,程序就可能会出现异常,比如:程序出现异常行为、得到的结果不一致等。

那么如何实现一个单例模式?

// 设计一个类,只能创建一个对象(单例模式)
class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{
		return _sins;
	}
	void Insert(string name, int salary)
	{
		_info[name] = salary;
	}
	void Print()
	{
		for (auto kv : _info)
		{
			cout << kv.first << " : " << kv.second << endl;
		}
	}
private:
	InfoSingleton()
	{}
    InfoSingleton(const InfoSingleton& info) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;
	map<string, int> _info;
private:
	static InfoSingleton _sins;
};

如果想要控制类创建对象,首先就是将本来公有的构造函数限制起来,在之前的简单特殊类设计中,我们还是使用构造函数构造对象,因为之前的限制并没有限制构造对象的数量,但是这里的单例模式限制了构造对象的数量,只能有一个,所以这里的构造函数几乎可以说没什么用处,关键的在于一个私有的静态成员_sins,静态成员在类外初始化,并且可以调用的函数的返回值也是返回这个静态成员变量,那么就让这个类只能在main函数之前实例化出一个对象。所以这就是为什么这个模式也叫饿汉模式。

那么这个类如何使用呢?

这里举的例子中,这个类中有两个成员变量_info和 _sins , _info是一个关联式容器

在这里插入图片描述

但是饿汉模式也存在一些问题:

  1. 单例对象初始化是数据过多,导致程序启动慢
  2. 多个单例类如果有初始化依赖关系,饿汉模式无法控制

懒汉模式

懒汉模式大的变化就是将静态成员变量变为了对象指针,与此同时GetInstance函数也做出了一些改变:

// 懒汉模式
class InfoSingLenton
{
public:
	static InfoSingLenton& GetInstance()
	{
		if (_psins == nullptr)
		{
			_psins = new(InfoSingLenton);
		}
		return *_psins;
	}
	void Insert(string name, int salary)
	{
		_info[name] = salary;
	}
	void Print()
	{
		for (auto kv : _info)
		{
			cout << kv.first << " : " << kv.second << endl;
		}
	}
private:
	InfoSingLenton()
	{}
	InfoSingLenton(const InfoSingLenton& info) = delete;
	InfoSingLenton& operator=(const InfoSingLenton& info) = delete;
	map<string, int> _info;
private:
	static InfoSingLenton* _psins;
};

将静态成员变量设置为指针并初始化为nullptr,在GetInstance函数中进行判断,如果为空就创建对象,依然和饿汉模式一样对拷贝构造和赋值重载进行封锁,很好地保证了在第一次获取单例对象的时候再创建对象,避免了饿汉模式的缺点,但是也带来了一些线程安全的问题:

如果多个线程在调用GetInstance函数的时候,此时_psins还是nullptr,那么就会有几个线程一起进入if判断内部new新的空间对已近new好的空间进行覆盖,而被覆盖的旧空间也会有内存泄露的问题。所以这里需要加锁。

先来看第一中加锁方式:

// 懒汉模式
class InfoSingLenton
{
public:
	static InfoSingLenton& GetInstance()
	{
        _smtx.lock();
		if (_psins == nullptr)
		{
			_psins = new(InfoSingLenton);
		}
        _smtx.unlock();
		return *_psins;
	}
	void Insert(string name, int salary)
	{
		_info[name] = salary;
	}
	void Print()
	{
		for (auto kv : _info)
		{
			cout << kv.first << " : " << kv.second << endl;
		}
	}
private:
	InfoSingLenton()
	{}
	InfoSingLenton(const InfoSingLenton& info) = delete;
	InfoSingLenton& operator=(const InfoSingLenton& info) = delete;
	map<string, int> _info;
private:
	static InfoSingLenton* _psins;
    static mutex _smtx;
};

上面的代码就解决了多线程的问题,但是会有另外的一个值得优化的问题存在:如果空间开辟成功,那么但是调用InfoSingLenton函数时每次都要加锁解锁,第二个问题就是这里的new如果抛异常,那么就会导致没有解锁的问题。

对于每次调用函数都会加锁解锁的问题,这里可以使用双重判断的方法,在这一层判断外再加一层if判断来判断_psins是否为空;而抛异常的问题就只需要对其进行捕获解锁后重新抛出。

	static InfoSingLenton& GetInstance()
	{
		if (_psins == nullptr)
		{
			_smtx.lock();
			try
			{
				if (_psins == nullptr)
				{
					_psins = new(InfoSingLenton);
				}
			}
			catch (...)
			{
				_smtx.unlock();
				throw;
			}
			_smtx.unlock();
		}
		return *_psins;
	}

仔细观察这里,其实我们可以将锁封装成一个对象,RAII的思想这不就用起来了嘛:

// RAII锁管理
template<class Lock>
class LockGuard
{
private:
	Lock& _lk;
public:
	LockGuard(Lock& lk)
		:_lk(lk)
	{
		_lk.lock();
	}
	~LockGuard()
	{
		_lk.unlock();
	}
};

在构造函数部分进行加锁,在析构函数部分解锁。这个类的构造函数和成员变量有个细节:因为锁是不可以拷贝的,所以这里的成员变量是一个锁的引用,那么在初始化列表处直接进行赋值即可。

// 懒汉模式
class InfoSingLenton
{
public:
	static InfoSingLenton& GetInstance()
	{
		if (_psins == nullptr)
		{
			LockGuard<mutex> lock(_smtx);
			if (_psins == nullptr)
			{
				_psins = new(InfoSingLenton);
			}
		}
		return *_psins;
	}
	void Insert(string name, int salary)
	{
		_info[name] = salary;
	}
	void Print()
	{
		for (auto kv : _info)
		{
			cout << kv.first << " : " << kv.second << endl;
		}
	}
private:
	InfoSingLenton()
	{}
	InfoSingLenton(const InfoSingLenton& info) = delete;
	InfoSingLenton& operator=(const InfoSingLenton& info) = delete;
	map<string, int> _info;
private:
	static InfoSingLenton* _psins;
	static mutex _smtx;
};
InfoSingLenton* InfoSingLenton::_psins = nullptr;
mutex InfoSingLenton::_smtx;

对象如何回收?

单例模式只可以创建一个对象,通过上面的学习我们可以知道,饿汉模式下这个对象创建在静态区中,懒汉模式中这个对象创建在堆上;其实这个对象回不回收都影响不大,程序在正常退出后,无论是在堆上创建的对象还是在静态区中创建的对象,操作系统都可以帮我们回收,但是在实际生产应用中有一些资源需要保存,这就要求我们必须手动处理。这里以懒汉模式举例:

这里思路就是创建一个static函数,函数中实现了对数据的保存和对堆空间上对象的清理,但是要注意这个函数也是存在线程安全问题的,需要加锁。

static void DelInstance()
{
    // 保存数据到文件
    // ...
    LockGuard<mutex> lock(_smtx);
    if (_psins)
    {
        delete _psins;
        _psins = nullptr;
    }
}

那如果我忘记调用这个函数怎么办呢?可不可以自动调用这个函数?

这里使用内部类解决了这个问题:内部设计一个类GC,GC的析构函数调用DelInstance函数,增加一个静态成员变量为GC类型并在类外初始化,那么程序结束时调用这个对象的析构函数就自动调用了DelInstance函数,在GC的析构函数也可以加一个if判断,如果已经手动调用回收函数,就避免了重复调用。

// 懒汉模式
class InfoSingLenton
{
public:
	static void DelInstance()
	{
		// 保存数据到文件
		// ...
		LockGuard<mutex> lock(_smtx);
		if (_psins)
		{
			delete _psins;
			_psins = nullptr;
		}
	}
	class GC
	{
	public:
		~GC()
		{
			if (_psins)
			{
				DelInstance();
			}
		}
	};
	static InfoSingLenton& GetInstance()
	{
		if (_psins == nullptr)
		{
			LockGuard<mutex> lock(_smtx);
			if (_psins == nullptr)
			{
				_psins = new(InfoSingLenton);
			}
		}
		return *_psins;
	}
	void Insert(string name, int salary)
	{
		_info[name] = salary;
	}
	void Print()
	{
		for (auto kv : _info)
		{
			cout << kv.first << " : " << kv.second << endl;
		}
	}
private:
	InfoSingLenton()
	{}
	InfoSingLenton(const InfoSingLenton& info) = delete;
	InfoSingLenton& operator=(const InfoSingLenton& info) = delete;
	map<string, int> _info;
private:
	static InfoSingLenton* _psins;
	static mutex _smtx;
	static GC _gc;
};
InfoSingLenton* InfoSingLenton::_psins = nullptr;
mutex InfoSingLenton::_smtx;
InfoSingLenton::GC InfoSingLenton::_gc;

总结

通过对上面这些特殊类的设计,可以发现如果想要对对象的创建进行限制,首先要做的就是将构造函数进行限制,其次就要注意拷贝构造和赋值重载的控制。其次单例模式也是需要掌握的,代码中有很多的细节。

希望大家学习愉快,早日拿到心仪的offer!
foSingLenton& operator=(const InfoSingLenton& info) = delete;
map<string, int> _info;
private:
static InfoSingLenton* _psins;
static mutex _smtx;
static GC _gc;
};
InfoSingLenton* InfoSingLenton::_psins = nullptr;
mutex InfoSingLenton::_smtx;
InfoSingLenton::GC InfoSingLenton::_gc;


**总结**

通过对上面这些特殊类的设计,可以发现如果想要对对象的创建进行限制,首先要做的就是将构造函数进行限制,其次就要注意拷贝构造和赋值重载的控制。其次单例模式也是需要掌握的,代码中有很多的细节。

> 希望大家学习愉快,早日拿到心仪的offer!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Feng,

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

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

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

打赏作者

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

抵扣说明:

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

余额充值