C++很多场景下会需要一些特殊要求的类,比如不能拷贝、只能在栈上实例化对象这些。来看看怎么实现吧。

不能被拷贝

拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
C++98和C++11有着不同的实现方式。
C++98,需要将拷贝和赋值私有化:

class CopyBan
{
public:

private:
	CopyBan(const CopyBan&);
	CopyBan& operator=(const CopyBan&);
};

在这里插入图片描述

error C2248: “CopyBan::CopyBan”: 无法访问 private 成员(在“CopyBan”类中声明)

C++11则比较简单,只需要禁止编译器自动生成拷贝和赋值即可:

class CopyBan
{
public:
	CopyBan() = default;
	CopyBan(const CopyBan&) = delete;
	CopyBan& operator=(const CopyBan&) = delete;

private:
};

在这里插入图片描述

error C2280: “CopyBan::CopyBan(const CopyBan &)”: 尝试引用已删除的函数

只能在堆上创建对象

1.首先我们要将构造函数私有化,然后提供一个静态成员函数来完成对象创建:

class HeapOnly
{
public:
	static HeapOnly* CreatObject()
	{
		return new HeapOnly;
	}
private:
	HeapOnly()=default;
};

当然这是不够的,因为还可以通过下面方式创建对象:

int main()
{
	HeapOnly ho1 = *HeapOnly::CreatObject();
	HeapOnly ho2(*HeapOnly::CreatObject());

	return 0;
}

2.因此我们还需要将拷贝和赋值给私有化或者禁掉

class HeapOnly
{
public:
	static HeapOnly* CreatObject()
	{
		return new HeapOnly;
	}
private:
	HeapOnly() = default;
	HeapOnly(const HeapOnly&) = default;
	HeapOnly& operator=(const HeapOnly&) = default;
};

只能在栈上创建对象

同上一样的方法:

class StackOnly
{
public:
	static StackOnly CreatObject()
	{
		return StackOnly();
	}
private:
	StackOnly() = default;
	StackOnly(const StackOnly&) = default;
	StackOnly& operator=(const StackOnly&) = default;
};

当然,如果你想要允许拷贝和赋值,可以改为禁掉new和delete:

class StackOnly
{
public:
	static StackOnly CreatObject()
	{
		return StackOnly();
	}
private:
	StackOnly() = default;
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;
};

也是一样的效果。

不能被继承

这个还是有两个版本。C++98上是将构造函数私有化,派生类调不动基类构造函数,无法继承:

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

C++11在类的继承里也讲过,只需要加上final关键即可:

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

单例模式

设计模式

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

像之前写过的适配器模式、迭代器模式之类的。

单例模式

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

单例模式的两种实现方式:

  • 饿汉模式

在程序启动前先实例化一个对象:

class ConfigInfo
{
public:
	static ConfigInfo* GetInstance()
	{
		return &_sInfo;
	}

	string GetIp()
	{
		return _ip;
	}

	void SetIp(const string& ip)
	{
		_ip = ip;
	}

private:
	ConfigInfo()
	{
		cout << "ConfigInfo()" << endl;
	}

	ConfigInfo(const ConfigInfo&) = delete;
	ConfigInfo& operator=(const ConfigInfo&) = delete;
private:
	string _ip = "127.0.0.1";
	int _port = 80;
	//...
	// 声明 
	static ConfigInfo _sInfo;
};

// 定义
ConfigInfo ConfigInfo::_sInfo;

类似如此,把构造函数、拷贝构造和赋值重载通通私有化。然后在成员变量里放唯一的静态成员变量
要注意我们的静态成员变量规则,在类内部的静态成员变量只是声明而不是定义,因此还需要在全局实例化成员变量。

这种实现方式优点自然是简单轻便,缺点是
1.很多单例类,都是饿汉模式,有些单例对象初始化资源很多,导致程序启动慢,迟迟进不了main函数
2.如果两个单例类有初始化依赖关系,饿汉也无法解决。因为我们无法控制哪个单例先实例化。

  • 懒汉模式

第一次使用实例对象时,创建对象。
实现方式非常简单:

class ConfigInfo
{
public:
	static ConfigInfo* GetInstance()
	{
		static ConfigInfo info;
		return &info;
	}

	string GetIp()
	{
		return _ip;
	}

	void SetIp(const string& ip)
	{
		_ip = ip;
	}

private:
	ConfigInfo()
	{
		cout << "ConfigInfo()" << endl;
	}
	ConfigInfo(const ConfigInfo&) = delete;
	ConfigInfo& operator=(const ConfigInfo&) = delete;
private:
	string _ip = "127.0.0.1";
	int _port = 80;
	//...
};

在GetInstance上以static方式初始化变量并返回,需要注意这种方式只能在C++11以后的版本使用。
在C++11之前的static初始化不保证线程安全,意思是可能导致内存泄漏
所以在C++11以前的版本要通过锁和检查的方式完成初始化:

class ConfigInfo
{
public:
	static ConfigInfo* GetInstance()
	{
		unique_lock<mutex> lock(_mtx);
		if (_spInfo == nullptr)  // 线程安全
		{
			_spInfo = new ConfigInfo;
		}
		return _spInfo;
	}

	string GetIp()
	{
		return _ip;
	}

	void SetIp(const string& ip)
	{
		_ip = ip;
	}

private:
	ConfigInfo()
	{
		cout << "ConfigInfo()" << endl;
	}

	ConfigInfo(const ConfigInfo&) = delete;
	ConfigInfo& operator=(const ConfigInfo&) = delete;
private:
	string _ip = "127.0.0.1";
	int _port = 80;
	//...

	static ConfigInfo* _spInfo;
	static mutex _mtx;
};
ConfigInfo* ConfigInfo::_spInfo = nullptr;
mutex ConfigInfo::_mtx;

当然我们发现ConfigInfo函数里的锁只是用来防止第一次初始化,但是后面每次调用都要加锁是十分影响效率的。因此考虑双检查法加锁来增加效率:

static ConfigInfo* GetInstance()
{
	// 双检查加锁
	if (_spInfo == nullptr)      // 性能
	{
		unique_lock<mutex> lock(_mtx);
		if (_spInfo == nullptr)  // 线程安全
		{
			_spInfo = new ConfigInfo;
		}
	}
	return _spInfo;
}

这样只有在第一次初始化的时候才会加锁,后面调用都不会加锁了。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐