【C++】特殊类设计

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

拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝只需让该类不能调用拷贝构造函数以及赋值运算符重载即可

  • C++98
    将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan
{
    // ...
    
private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};

为什么只声明不实现呢?
因为自己不声明,库就会自动生成。因此只要自己写库就不会默认生成。
为什么声明私有呢?
如果声明为公有类外面可以实现别人就可以调用。

  • C++11
    C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
class CopyBan
{
    // ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

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

1.首先我们必须要把构造函数私有

如果不把构造私有分分钟钟就创建出三个对象,并且这三个对象在不同的位置栈、堆、静态区。

class HeapOnly
{

};

int main()
{
	HeapOnly hp1;
	HeapOnly* php2 = new HeapOnly;
	static HeapOnly hp3;

	return 0;
}

因此把构造私有

class HeapOnly
{
private:
	HeapOnly()
	{}
};

int main()
{
	HeapOnly hp1;
	HeapOnly* php2 = new HeapOnly;
	static HeapOnly hp3;

	return 0;
}

这样类外面就调不了构造函数。外面就不随便创建对象!

在这里插入图片描述

但是我还是需要创建需要的对象,因此类内部写一个创建对象的成员函数

class HeapOnly
{
public:
	HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
private:
	HeapOnly()
	{}
};

但是现在问题就来了,外面根本不能创建出对象,也就是说根本调不动CreteObj。

那怎么办呢?

2.提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建

静态成员函数没有this指针,可以不用对象去调用,使用类域::类的成员函数调用

class HeapOnly
{
public:
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
private:
	HeapOnly()
	{}
};

int main()
{
	//HeapOnly hp1;
	//HeapOnly* php2 = new HeapOnly;
	//static HeapOnly hp3;

	HeapOnly* php4=HeapOnly::CreateObj();
	return 0;
}

现在这个写法有没有什么地方没有考虑到?
就是不只在堆上,还可以在别的地方创建出对象,假如是栈。

int main()
{
	HeapOnly* php4=HeapOnly::CreateObj();
	//hp5在栈上!
	HeapOnly hp5(*php4);//拷贝构造
	
	return 0;
}

3.防拷贝

class HeapOnly
{
public:
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
private:
	HeapOnly()
	{}

	HeapOnly(const HeapOnly&) = delete;
	//这里赋值不用delete的,自己不写编译器默认生成的是浅拷贝,对象还是在堆上
};

在这里插入图片描述

这是一种完整方法,只能在堆上创建对象!

还有一种方法析构函数私有+拷贝构造delete

class HeapOnly
{
public:
	HeapOnly()
	{}
	
private:
	~HeapOnly()
	{}

	HeapOnly(const HeapOnly&) = delete;
};

int main()
{
	HeapOnly hp1;

	return 0;
}

在这里插入图片描述

原因在于出了作用域会调用析构函数,这里不让调用编译就报错了。

这种写法确实只可以在堆上申请对象!但是有没有什么问题?

int main()
{
	//HeapOnly hp1;

	HeapOnly* php2 = new HeapOnly;

	return 0;
}

在这里插入图片描述

最显然就是没有办法delete释放资源了,有没有什么办法解决一下?

在类内部写一个Destory去释放资源。

class HeapOnly
{
public:
	HeapOnly()
	{}

	void Destory()
	{
		delete this;
		//或者调用显示调用析构1:对象.析构  2:this->析构
		//this->~HeapOnly();
	}

private:
	~HeapOnly()
	{}

	HeapOnly(const HeapOnly&) = delete;
};

int main()
{
	HeapOnly* php2 = new HeapOnly;
	php2->Destory();
	return 0;
}

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

方法1:同上将构造函数私有化,然后设计静态方法创建对象返回

class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		return StackOnly();
	}

private:
	StackOnly()
	{}
};

int main()
{
	StackOnly so1=StackOnly::CreateObj();
	return 0;
}

还有一种方法说的禁掉new和delete。
因为new在底层调用void* operator new(size_t size),只需要将该函数屏蔽掉即可。
注意:也要防止定位new

class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		return StackOnly();
	}

	void* operator new(size_t size) = delete;
	void* operator delete(size_t size) = delete;
//private:
	StackOnly()
	{}
};

int main()
{
	StackOnly so1=StackOnly::CreateObj();
	StackOnly* pso = new StackOnly;
	//静态封不掉
	static StackOnly so2;
	return 0;
}

这种方法是有问题的,如果只禁用operator new、operator delete,只是不想在堆上创建对象,但是可以在除了堆上的其他地方创建对象,全局、局部、静态都可以!

在这里插入图片描述

如果加上也可以,但是还是要把构造给封掉,不然静态还是可以的。

方法1的代码还有没有其他问题?
有的,方法1并没有把路封死!

class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		return StackOnly();
	}

private:
	StackOnly()
	{}
};

int main()
{
	StackOnly so1=StackOnly::CreateObj();
	static StackOnly so2 = StackOnly::CreateObj();

	return 0;
}

静态对象还可以实现
在这里插入图片描述

那把拷贝构造封掉试一试

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

但是这里又不行了

在这里插入图片描述

这里拷贝构造不行了,那我提供一个移动构造

class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		return StackOnly();
	}

	StackOnly(StackOnly&&)
	{}
	
	StackOnly(const StackOnly&) = delete;
private:
	StackOnly()
	{}
};

但这里又行了

在这里插入图片描述

这里想说的是这个类封不死!静态的确实没有很好的办法解决。
如果真的把这里封死就一种办法,把拷贝构造封死,不让用的人这样使用。

class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		return StackOnly();
	}

	//StackOnly(StackOnly&&)
	//{}

	void Print() const
	{
		cout << "StackOnly::Print()" << endl;
	}

	StackOnly(const StackOnly&) = delete;
private:
	StackOnly()
	{}
};

int main()
{
	//不用这样使用
	//StackOnly so1=StackOnly::CreateObj();
	//static StackOnly so2 = StackOnly::CreateObj();
	
	//而是这样用
	//1.
	StackOnly::CreateObj().Print();
	//2.
	const StackOnly& so1= StackOnly::CreateObj();
	so1.Print();

	return 0;
}

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

  • C++98方式
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
	static NonInherit GetInstance()
	{
		return NonInherit();
	}
private:
	NonInherit()
	{}
};

子类继承父类,必须要调用父类的构造函数,如果自己没有调用显示写调用父类,编译器就会自动调用父类的构造函数,但是父类构造私有了因此就会报错。即使在子类种显示写了调用父类构造,但是因为父类构造还是私有的,还会报错。因此这个父类就不能被子类继承!

  • C++11方法
    final关键字,final修饰类,表示该类不能被继承
class A  final
{
    // ....
};

5.请设计一个类,只能创建一个对象(单例模式)

设计模式:

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

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

设计模式有二十多种但是常见就那么几种。C++常见的就是迭代器模式、适配器模式、单例模式,今天就主要说说单例模式,有兴趣的可以在扩展了解工厂模式、观察者模式。

单例模式:

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

任何一个类都可以写成单例模式。单例模式的类的特定:全局只有一个唯一对象。

现在考虑这样一个问题,如何让一个类全局只有一个唯一对象呢?
和上面的思路是一样的。首先还是让构造私有!不然就随意创建对象了。

class InfoSingleton
{
public:

private:
	InfoSingleton()
	{}

	map<string, int> _info;
	//...
};

int main()
{
	//InfoSingleton s1;
	//InfoSingleton s2;
	//InfoSingleton s3;
	
	return 0;
}

其次单例一般都会提供一个GetInstance()的静态成员函数去获取全局的唯一对象。

class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{

	}

private:
	InfoSingleton()
	{}

	map<string, int> _info;
	//...
};

该如何获取呢全局的唯一对象呢?
直接定义一个全局的行不行?

class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{

	}

private:
	InfoSingleton()
	{}
	
	map<string, int> _info;
	//...
};

//类外直接定义一个全局的
InfoSingleton ins;

是不行的,这里调用不了构造函数!

我们先见见第一种方法

class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{
		//直接返回就行
		return _sins;
	}

private:
	InfoSingleton()
	{}

	map<string, int> _info;
	//...

private:
	static InfoSingleton _sins;//类中声明成静态的成员

};

InfoSingleton InfoSingleton::_sins;//类外定义,因为它的类里面的因此可以调用构造函数

可以认为静态的成员就是全局的,生命周期也是全局的。并且在main之前就初始化了。它和定义一个全局的没有任何区别,只是属于这个类域而已。

想获取这个对象就直接使用类域::成员函数。

int main()
{
	InfoSingleton::GetInstance();	
	return 0;
}

假设这个类提供一些其他函数,我们就可以如下访问。

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()
	{}

	map<string, int> _info;
	//...

private:
	static InfoSingleton _sins;//类中声明成静态的成员

};
InfoSingleton InfoSingleton::_sins;//类外定义,因为它的类里面的因此可以调用构造函数

//InfoSingleton ins;

int main()
{
	//1.
	InfoSingleton::GetInstance().insert("张三",10000);
	//2.
	InfoSingleton& infos1 = InfoSingleton::GetInstance();
	infos1.insert("李四", 15000);
	infos1.insert("王五", 30000);

	infos1.Print();
	return 0;
}

在这里插入图片描述

单例模式有两种实现模式:

  • 第一种:饿汉模式
    就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象(也就是说一开始(main函数之前)就创建对象),上面那种写法就是饿汉模式。

但是目前现在写的这种方法有一些不好的地方。保证不了单例!

int main()
{
	//1.
	InfoSingleton::GetInstance().insert("张三",10000);
	//2.
	InfoSingleton& infos1 = InfoSingleton::GetInstance();
	infos1.insert("李四", 15000);
	infos1.insert("王五", 30000);

	infos1.Print();

	//拷贝构造
	InfoSingleton copy = InfoSingleton::GetInstance();
	copy.insert("赵六", 20000);
	copy.Print();

	infos1.Print();
	return 0;
}

在这里插入图片描述

现在已经产生了两个对象了。
因此这里也要禁掉拷贝构造,甚至我也不想要赋值发生!

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;
		}
		cout << endl;
	}

private:
	InfoSingleton()
	{}

	InfoSingleton(const InfoSingleton& info) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;

	map<string, int> _info;
	//...

private:
	static InfoSingleton _sins;//类中声明成静态的成员

};
InfoSingleton InfoSingleton::_sins;//类外定义,因为它的类里面的因此可以调用构造函数

//InfoSingleton ins;

int main()
{
	//1.
	InfoSingleton::GetInstance().insert("张三",10000);
	//2.
	InfoSingleton& infos1 = InfoSingleton::GetInstance();
	infos1.insert("李四", 15000);
	infos1.insert("王五", 30000);

	infos1.Print();

	//拷贝构造
	//InfoSingleton copy = InfoSingleton::GetInstance();
	//copy.insert("赵六", 20000);
	//copy.Print();

	infos1.Print();
	return 0;
}

饿汉模式的优点

  1. 简单

饿汉模式的缺点

  1. 单例对象初始化数据太多,导致启动慢(因为在main函数之前就会初始化)
  2. 多个单例有初始化依赖关系,饿汉模式无法控制
    在这里插入图片描述
    因为饿汉都是全局对象,全局对象到底谁先初始化谁后初始化,主要看编译器自己。同一个文件有先后顺序,不同文件先后顺序很难自己控制!

基于这些问题,单例还有另一种模式

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

不要一开始就创建,用的时候在创建!并且只在第一次的时候创建对象,其他直接返回即可。

饿汉模式 —> 静态对象
懒汉模式 —> 静态指针

class InfoSingleton
{
public:
	//这里可以也可以返回指针
	static InfoSingleton& GetInstance()
	{
		//第一次获取单例对象的时候创建对象
		if (_psins == nullptr)
		{
			_psins = new InfoSingleton;
		}
		return *_psins;
	}

	void insert(string name, int salary)
	{
		_info[name] = salary;
	}

	void Print()
	{
		for (auto& kv : _info)
		{
			cout << kv.first << " : " << kv.second << endl;
		}
		cout << endl;
	}

private:
	InfoSingleton()
	{}

	InfoSingleton(const InfoSingleton& info) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;

	map<string, int> _info;
	//...

private:
	static InfoSingleton* _psins;

};
InfoSingleton* InfoSingleton::_psins=nullptr;

在这里插入图片描述
懒汉模式把上面的问题都给解决了

懒汉模式的优点

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

懒汉模式的缺点

  1. 复杂

接下来我们看看复杂在那些方面

目前这段代码有没有什么问题?

在这里插入图片描述

多个线程一起调用GetInstace(),存在线程安全的风险

有两个线程来了之后,可能都会进入到if里面,会new两次,最终_psins指向的是第二个线程new出来的对象,这就有问题了。

如何解决呢?
加锁!

注意因为GetInstace()是静态成员函数,没有this指针只能访问静态成员变量,因此锁也要是一个静态的锁!

class InfoSingleton
{
public:
	//这里可以也可以返回指针
	static InfoSingleton& GetInstance()
	{
		_smtx.lock();
		if (_psins == nullptr)
		{
			_psins = new InfoSingleton;
		}
		_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;
		}
		cout << 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;

};
InfoSingleton* InfoSingleton::_psins=nullptr;
mutex InfoSingleton::_smtx;

加锁之后就是线程安全的了,但是这样加锁有没有什么问题?
在这里插入图片描述

我只想保证第一次加锁解锁后面不需要加锁解锁了。但是现在每次线程进来都需要加锁解锁,但加锁解锁也是需要资源的!

能不能把锁放在if里面?

在这里插入图片描述
这样是不行的!可能两个线程还是都进入if里面,其他一个线程先加锁new一个对象然后解锁,但是时间片到了就切走了,另一个线程就开始加锁然后也new一个对象最后解锁,现在就是线程不安全的了!

这里真正的解决办法:双检查加锁

不仅是这里,以后只要是想保护第一次申请都可以用这种方法!

static InfoSingleton& GetInstance()
{
	//第一次if,对象new出来之后,避免每次都加锁的检查,提高性能
	if (_psins == nullptr)
	{
		_smtx.lock();
		//第二次if,保证线程安全且只能new一次
		if (_psins == nullptr)
		{
			_psins = new InfoSingleton;
		}
		_smtx.unlock();
	}

	return *_psins;
}

饿汉模式没有线程安全吗?需要加锁吗?
饿汉模式没有线程安全的问题,因此不需要加锁!main函数之前没有可能存在两个线程去调用GetInstance(),在main函数之前这个单例模式的静态对象就已经初始化好了。而这个线程是自己些的并且在main函数之后才会调用的。但是对象已经创建好了因此不存在线程安全的问题。

再看这段代码,还有没有什么问题?

在这里插入图片描述

new可能抛异常,造成没解锁

第一次解决方法就是捕捉异常

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

		return *_psins;
	}

但是这种写法有点挫。

第二种解决方法RAII管理锁
构造时加锁,析构时解锁!
可以自己写一个RAII锁管理类

//RAII锁管理类
template<class Lock>
class LockGuard
{
public:
	LockGuard(Lock& mtx)
		:_mtx(mtx)//锁不能拷贝构造,1.传锁的地址过来  2.私有成员给个引用
	{
		_mtx.lock();
	}

	~LockGuard()
	{
		_mtx.unlock();
	}
private:
	Lock& _mtx;//引用的成员变量,必须在初始化列表初始化
};


class InfoSingleton
{
public:
	//这里可以也可以返回指针
	static InfoSingleton& GetInstance()
	{
		if (_psins == nullptr)
		{
			LockGuard<mutex> lockguard(_smtx);
			//_smtx.lock();
			if (_psins == nullptr)
			{
				_psins = new InfoSingleton;
			}
			//_smtx.unlock();
		}

		return *_psins;
	}
//。。。。
};

或者用库里提供的RAII风格的锁
在这里插入图片描述

static InfoSingleton& GetInstance()
{
	if (_psins == nullptr)
	{
		std::lock_guard<mutex> lock(_smtx);
		if (_psins == nullptr)
		{
			_psins = new InfoSingleton;
		}	
	}
	return *_psins;
}

现在这个代码还有没有可以优化的呢?
可能会想到释放的问题。
一般单例对象不需要考虑释放!单例对象在整个程序运行期间都要在都要用,所以一般不需要释放,那new了就不delete会有问题吗?其实也并没有什么问题。进程正常结束之后会清理资源。

有些地方需要考虑释放问题: 如单例对象不用时,一些资源需要保存,必须要手动处理!
就可以提供一个静态的DelInstance()

static void DelInstance()
{
	//保存数据到文件
	//...

	std::lock_guard<mutex> lock(_smtx);
	if (_psins)
	{
		delete _psins;
		_psins = nullptr;
	}
}

如果忘记调用,但是必须要把数据存到文件中呢?
可以内部再实现一个类,实现自动回收!

class InfoSingleton
{
public:
	static InfoSingleton& GetInstance()
	{
		if (_psins == nullptr)
		{
			std::lock_guard<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)
			{
				DelInstance();//所以这里可以直接调
			}
		}
	};
//。。。

private:
	static InfoSingleton* _psins;
	static mutex _smtx;
	static GC _gc;
};
InfoSingleton* InfoSingleton::_psins=nullptr;
mutex InfoSingleton::_smtx;
InfoSingleton::GC InfoSingleton::_gc;//定义一个GC类的对象,main函数结束之后会自动调用GC的析构函数

这两种写法既可以手动调用主动回收,也可以让它在程序结束之后自动回收。

见识到懒汉的复杂之处了把,其实懒汉还有一种非常方便的写法。
懒汉模式二

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;

	map<string, int> _info;
	//...
};

这种写法能不能称为懒汉?
可以的。懒汉只有在第一次调用的时候在创建对象。
现在这个局部静态对象它就是main函数之前初始化,还是在main函数之后初始化的?

在这里插入图片描述

局部静态是在main函数之后才会初始化,并且只会在第一次调用才会调用构造函数,第二次开始之后就不会在调用构造了,而是直接用。

在这里插入图片描述

所以这种写法就是懒汉模式。

但是这种写法需要注意一些问题:

  1. 是懒汉,因为静态的局部变量是在main函数之后才创建初始化的
  2. C++11之前,这里是不能保证static的初始化是线程安全的
  3. C++11之后,可以
  • 56
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 70
    评论
评论 70
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值