【C++】特殊类设计

2023-10-15 12:23:38 浏览数 (2)

设计一个类,不能被拷贝

一个类不能被拷贝,那么就让该类不能调用拷贝构造与赋值运算符重载。所以想要让一个类禁止拷贝:

C 98的方式是将拷贝构造函数与赋值运算符重载只声明不定义,为什么只声明不实现:如果不声明的话,实现不知道实现什么样的,不能被拷贝,没必须要实现了;拷贝构造不声明会自动默认生成;为什么是私有:如果共有别人可以在类外实现,可以被使用

C 11直接使用delete,在默认成员函数后=delete

并且将权限设置为私有

代码语言:javascript复制
class CopyBan
{
public:
	CopyBan()
	{}
private:
	//C  98
	CopyBan(const CopyBan& cb);
	CopyBan& operator=(const CopyBan&);
	//C  11
	//CopyBan(const CopyBan&) = delete;
	//CopyBan& operator=(const CopyBan&) = delete;
};

设计一个类,只能在堆上创建

只能在堆上创建,也就是只能通过new创建对象:

将构造函数设置为私有,防止外部进行调用构造函数在栈上创建对象

提供获取对象的static接口,该接口在堆上创建一个对象返回(向外部提供的CreateObj函数必须设置为静态成员函数,因为外部调用该接口就是为了获取对象的,而非静态成员函数必须通过对象才能调用,这就变成鸡生蛋蛋生鸡的问题了…)

将拷贝构造函数设置为私有,只声明不实现,防止外部调用拷贝构造函数在栈上创建对象

代码语言:javascript复制
//设计一个类,只能在堆上创建对象
class HeapOnly
{
public:
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
private:
	HeapOnly()
	{}
    //C  98
	HeapOnly(const HeapOnly&);//防止拷贝
    //C  11
	//HeapOnly(const HeapOnly&) = delete;
};

设计一个类,只能在栈上创建

法一:

将构造函数设置为私有,防止外部直接调用构造函数在堆上创建对象

向外部提供一个获取对象的static接口,该接口在栈上创建一个对象并返回

代码语言:javascript复制
class StackOnly
{
public:
	static StackOnly Createobj()
	{
		return StackOnly();
	}
private:
    //将构造函数设置为私有
	StackOnly()
    {}
};

但是仔细发现,上面存在缺陷,不能防止外部调用拷贝构造函数创建对象:

代码语言:javascript复制
int main()
{
	StackOnly so1 = StackOnly::Createobj();
	static StackOnly so2(so1);
	StackOnly* so3 = new StackOnly(so1);
	return 0;
}

但是我们不能将拷贝构造函数设为私有,也不能用=delete的方式将拷贝构造函数删除,因为CreateObj函数当中创建的是局部对象,返回局部对象的过程需要调用拷贝构造函数。

法二:屏蔽operator new函数和operator delete函数。

代码语言:javascript复制
//设计一个类,只能在栈上创建对象
class StackOnly
{
public:
	static StackOnly Createobj()
	{
		return StackOnly();
	}
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;
	StackOnly()
	{}
};

new在堆上申请空间:通过operator new函数申请空间,然后在申请的空间上执行构造函数

delete在堆上释放空间:通过在该空间执行析构函数,资源清理,然后通过调用operator delete函数释放对象空间

new和delete默认调用全局的operator new函数与operator delete函数,只要把operator new与operator delete函数屏蔽掉,那么就无法在使用new在堆上创建对象了。

该方法也有一个缺陷,就是无法防止外部在静态区创建对象。

代码语言:javascript复制
static StackOnly so2(so1);

设计一个类,不能被继承

C 98:该类的构造函数设置为私有即可。派生类中调不到基类的构造函数,无法继承。派生类的构造函数调用时,必须调用父类的构造函数初始化父类的那一部分成员,父类的私有成员在子类不可见,所以创建子类对象时无法调用父类的构造函数对父类的成员进行初始化,因此该类被继承后子类不能创建出对象。

代码语言:javascript复制
class NonInherit
{
public:
	static NonInherit GetInstance()
	{
		return NonInherit();
	}
private:
    //构造函数私有
	NonInherit()
	{}
};

C 11:final关键字,final修饰类,表示该类不能被继承。此时继承后没有创建对象也会编译报错

代码语言:javascript复制
class A final
{
  //...  
};

设计一个类,只能创建一个对象

单例模式

单例模式是一种设计模式(Design Pattern),设计模式就是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式的目的就是为了可重用代码、让代码更容易被他人理解、保证代码可靠性程序的重用性。

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

单例模式有两种实现方式,分别是饿汉模式懒汉模式

  • 饿汉模式

一开始(main函数之前)就创建对象

将构造函数设置为私有,同时将拷贝构造和赋值运算符重载函数也设置为私有或delete,防止外部创建或拷贝对象

提供static对象,在程序入口前完成该定义

提供全局获取单例对象GetInstance()

代码语言:javascript复制
//单例模式:全局只有唯一对象
//饿汉模式:一开始就创建对象(main函数之前)
class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{
		return _sins;
	}
private:
	InfoSingleton()
	{}
    InfoSingleton(InfoSingleton& info) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;
private:
	static InfoSingleton _sins;//声明
};
InfoSingleton InfoSingleton::_sins;//定义,属于类域::,可以调用构造函数

饿汉模式:main之前就创建完对象,缺点:

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

比如:A和B都是单例类,要求先初始化A,在初始化B,因为B会依赖A;饿汉模式无法控制顺序,全局对象谁先初始化控制不了

饿汉模式有没有线程安全的问题:不需要加锁,main函数之前没有多个线程调用GetIntance,创建线程是在main之后的,一般情况下写饿汉比较合适。所以饿汉模式下单例对象的创建是线程安全的。

  • 懒汉模式

将构造函数设置为私有,同时将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象

提供static指针,在程序入口前完成定义

提供全局获取单例对象GetInstance()

代码语言:javascript复制
//懒汉模式
class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{
		//第一次获取单例对象的时候创建对象
		if (_psins == nullptr)
		{
            //第一次获取单例对象的时候创建对象
			_psins = new InfoSingleton;
		}
		return *_psins;
	}
private:
	InfoSingleton()
	{}
	InfoSingleton(const InfoSingleton& info) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;

private:
	static InfoSingleton* _psins;
};
InfoSingleton* InfoSingleton::_psins=nullptr;

懒汉模式:第一次获取单例对象的时候创建对象

1.对象在main函数之后才会创建,不会影响启动顺序 2.可以主动控制顺序

懒汉模式问题:线程安全问题,多个线程一起调用GetInstance(),存在线程安全的风险,可能会new多个对象出来,所以需要加锁

GetInstance()是静态的成员函数,所以需要加上一个静态的mutex:

代码语言:javascript复制
//懒汉模式
class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{
		//第一次获取单例对象的时候创建对象
		_smtx.lock();
		if (_psins == nullptr)
		{
			_psins = new InfoSingleton;
		}
		_smtx.unlock();
		return *_psins;
	}
private:
	InfoSingleton()
	{}
	InfoSingleton(const InfoSingleton& info) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;
private:
	static InfoSingleton* _psins;
	static mutex _smtx;
};
InfoSingleton* InfoSingleton::_psins=nullptr;
mutex InfoSingleton::_smtx;
  • 双检查加锁

但是还是不够完美:只需要第一次加锁解锁,如果简单的将加锁解锁放在if语句前后,那么后面调用GetInstance函数获取创建好的单例对象时,会进行无意义的加锁操作,加锁会消耗,影响程序运行的效率

如何解决:双检查加锁,保证线程安全的检查(并不是冗余)。对于第一次需要加锁保护的场景可以使用双检查加锁,在加锁和解锁的外面在套上一次if判断。

所以对于以后申请一次的都可以双检查加锁!

代码语言:javascript复制
static InfoSingleton& GetInstance()
	{
		//第一次获取单例对象的时候创建对象
		if (_psins == nullptr)//对象new出来以后,避免每次都加锁的检查,提高性能
		{
			_smtx.lock();
			if (_psins == nullptr)//保证线程安全的检查且只new一次
			{
				_psins = new InfoSingleton;
			}
			_smtx.unlock();
		}
		return *_psins;
	}

懒汉补充完善:new会抛异常,没解锁,最好try catch

代码语言:javascript复制
    static InfoSingleton& GetInstance()
	{
		//第一次获取单例对象的时候创建对象
		if (_psins == nullptr)
		{
			_smtx.lock();
			try
			{
				if (_psins == nullptr)
				{
					_psins = new InfoSingleton;
				}
			}
			catch (...)
			{
				_smtx.unlock();
				throw;
			}
			_smtx.unlock();
		}
		return *_psins;
	}

这种做法还可以继续改进,利用智能指针改进try catch的写法:通过一个类LockGuard来进行加锁和解锁的管理:

代码语言:javascript复制
//RAII锁管理类
template<class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lk)
		:_lk(lk)//锁不允许拷贝,所以要加引用
	{
		_lk.lock();
	}
	~LockGuard()
	{
		_lk.unlock();
	}
private:
	Lock& _lk;//锁不允许拷贝,所以要加&
};
//懒汉模式
class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{
		//第一次获取单例对象的时候创建对象
		if (_psins == nullptr)
		{
			LockGuard<mutex> lock(_smtx);
			if (_psins == nullptr)
			{
				_psins = new InfoSingleton;
			}
		}
		return *_psins;
	}
private:
	InfoSingleton()
	{}
	InfoSingleton(const InfoSingleton& info) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;
private:
	static InfoSingleton* _psins;
	static mutex _smtx;
};
InfoSingleton* InfoSingleton::_psins = nullptr;
mutex InfoSingleton::_smtx;

库里面也有lock_guard, std::lock_guard

  • 单例对象的释放

对于释放问题:一般单例对象不需要考虑释放,new之后不delete会怎么样?没什么问题,new之后资源会释放,进程结束,也会清理资源。

单例对象不用时,考虑释放:

可在单例类中多加一个DelInstance函数,在该函数进行单例对象的释放,不在需要该单例对象就可以主动调用DelInstance释放单例对象

代码语言:javascript复制
	static void DelInstance()
	{
		//保存数据到文件
		//...
		std::lock_guard<mutex> lock(_smtx);
		if (_psins)
		{
			delete _psins;
			_psins = nullptr;
		}
	}

自动回收GC类:在单例类中实现一个内嵌垃圾回收类,在其析构函数中完成单例对象释放。

如果忘记调用?自动回收:内部类是外部类的友元,写一个GC类,在单例类中定义多一个GC对象,对象消耗调用其析构函数,对单例对象进行释放

代码语言:javascript复制
template<class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lk)
		:_lk(lk)//锁不允许拷贝,所以要加引用
	{
		_lk.lock();
	}
	~LockGuard()
	{
		_lk.unlock();
	}
private:
	Lock& _lk;
};
//懒汉模式
class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{
		//第一次获取单例对象的时候创建对象
		if (_psins == nullptr)
		{
			LockGuard<mutex> lock(_smtx);
			if (_psins == nullptr)
			{
				_psins = new InfoSingleton;
			}

		}
		return *_psins;
	}
	static void DelInstance()
	{
		//保存数据到文件
		//...
		std::lock_guard<mutex> lock(_smtx);
		if (_psins)
		{
			delete _psins;
			_psins = nullptr;
		}
	}
	
	class GC
	{
	public:
		~GC()
		{
			if (_psins)
			{
				cout << "~GC()" << endl;
				DelInstance();
			}
		}
	};

	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* _psins;
	static mutex _smtx;
	static GC _gc;//
};
InfoSingleton* InfoSingleton::_psins = nullptr;
mutex InfoSingleton::_smtx;
InfoSingleton::GC  InfoSingleton::_gc;//出了作用域调用析构函数,main函数结束


int main()
{
	InfoSingleton::GetInstance().Insert("张三", 10000);
	InfoSingleton& infos1 = InfoSingleton::GetInstance();
	infos1.Insert("李四", 10000);
	infos1.Insert("王五", 10000);
	infos1.Insert("小六", 10000);
	infos1.Print();
	return 0;
}
  • 其他懒汉版本写法

将构造函数设置为私有,并且将拷贝构造和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象

提供一个全局访问获取单例对象

代码语言:javascript复制
//懒汉模式
class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{
		static InfoSingleton sinst;
		return sinst; 
	}
private:
	InfoSingleton()
	{
		cout << "InfoSingleton()" << endl;
	}
	InfoSingleton(const InfoSingleton& info) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;
};

属于懒汉模式,因为局部静态变量不是在程序运行主函数之前初始化的,而是第一次调用GetInstance函数时初始化的

第一次调用GetInstance函数时才会定义这个静态的单例对象,保证了全局只有这个唯一实例

并且这里的单例对象定义过程是线程安全的:C 11之前不能保证sinst的初始化是线程安全的,C 11之后可以。

0 人点赞