C++--22.特殊类的设计

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

我们分析,当一个类只允许在堆上创建对象,原本,正常创建对象一定会调用构造函数,或者拷贝构造,要使用构造函数或者拷贝构造去创建对象(别人调用拷贝构造会在栈上生成对象),是不能保证只在堆上创建的,所以我们需要将构造函数与拷贝构造声明私有,因为无法通过构造函数与拷贝构造创建,所以我们需要引入一个静态成员函数,在此函数中创建对象

实现方式: 1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。 2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
class HeapOnly
{
public:
	static HeapOnly* GetObj()//设置个静态成员函数创建对象
	{
		return new HeapOnly;
	}

private:
	HeapOnly()
	{}

	// C++98防拷贝:声明成私有
	//HeapOnly(const HeapOnly& );
public:
	// C++11 : 声明成delete
	HeapOnly(const HeapOnly&) = delete;
};

int x7()
{
	//HeapOnly hp;
	//HeapOnly* p = new HeapOnly;
	//HeapOnly* p = HeapOnly::GetObj();
	std::shared_ptr<HeapOnly> sp1(HeapOnly::GetObj());
	std::shared_ptr<HeapOnly> sp2(HeapOnly::GetObj());

	//HeapOnly copy(*sp1);//调用拷贝构造构造函数

    system("pause");
	return 0;
}

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

同样的,当我们了解了只能在堆上创建对象的方式之后,我们可以仿照这种方式,创建一个仅能在栈上创建对象的类

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

class StackOnly
{
public:
	static StackOnly GetObj()
	{
		return StackOnly();
	}
private:
	StackOnly()
	{}
};

这样操作,就可以保证对象通过调用函数创建在栈上了

还有一种方式,就是直接禁掉new 函数,就不会在堆上创建对象了,但是这也有一个问题,就是无法避免在静态区创建的对象

// 这种方案存在一定程序缺陷,无法阻止在数据段(静态区)创建对象
class StackOnly
{
public:
	void* operator new(size_t size) = delete;
};

int x8()
{
	StackOnly so;
	//StackOnly* p = new StackOnly;

	static StackOnly sso;


	return 0;
}

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

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

这其实就是将拷贝构造私有就可以了,别人也就无法进行拷贝了

C++98 将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可
class CopyBan
{
 // ...
 
private:
 CopyBan(const CopyBan&);
 CopyBan& operator=(const CopyBan&);
 //...
};
原因: 1. 设置成私有:如果只声明没有设置成 private ,用户自己如果在类外定义了,就可以不能禁止拷贝了 2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
C++11 扩展 delete 的用法, delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上 =delete ,表示让编译器删除掉该默认成员函数。
class CopyBan
{
 // ...
 CopyBan(const CopyBan&)=delete;
 CopyBan& operator=(const CopyBan&)=delete;
 //...
};

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

这个其实就是将构造函数私有,而子类若要想继承就必须要调父类的构造函数,所以无法继承

C++98 方式
class NonInherit
{
public:
 static NonInherit GetInstance()
 {
 return NonInherit();
 }
private:
 NonInherit()
 {}
};
C++11 方法 fifinal 关键字, fifinal 修饰类,表示该类不能被继承
class A final
{
 // ....
};

单例模式

其实我们之前已经学过一些设计模式

迭代器模式--基于面向对象的三大特性之一,封装设计出来的,用一个迭代器封装以后,不暴露容器结构的情况下,统一的方式访问修改容器中的数据

 适配器模式 -- 体现的是一种复用
 还有一些常见的设计模式如:工厂模式、装饰器模式、观察者模式、单例模式...
 一个类只能在全局(进程中)只有一个实例对象,就是单例模式
 什么场景下使用?比如一个进程中有一个内存池,进程中的多线程需要内存都要到这个内存池中取,那么这个内存池的类就可以设计单例模式。

我们先来演示一个最简单的单例模式

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		if (_pinst == nullptr)
		{
			_pinst = new Singleton;
		}

		return _pinst;
	}

	Singleton(const Singleton& s) = delete;

private:
	Singleton()
	{}

	static Singleton* _pinst;
};

Singleton* Singleton::_pinst = nullptr;

我们可以看到,我们首先将构造函数声明私有,使其只能通过静态的get函数来创建对象,其次在get函数中设置,第一次调用就创建对象,之后创建的直接返回这个指针,此时不管我们调用多少回也都是这个指针了,这便完成了我们最简单的单例模式

但其实这个单例模式是有问题的,它存在线程安全的问题,当有两个线程同时去调用get函数时,可能会出现都检测出为空,然后都创建了一个对象,在随后的调用中又会将第一个创建的覆盖掉,此时会出现内存泄漏

我们的解决方案就是加锁

class Singleton
{
public:
	static Singleton* GetInstance()
	{
           _mtx.lock();
		if (_pinst == nullptr)
		{
			_pinst = new Singleton;
		}
            _mtx.unlock();

		return _pinst;
	}

	Singleton(const Singleton& s) = delete;

private:
	Singleton()
	{}

	static Singleton* _pinst;
    static mutex _mtx;//声明静态锁
};

Singleton* Singleton::_pinst = nullptr;
mutex Singleton::_mtx;//定义锁

但这样其实也并不是完美的,我们在new对象时是可能出现异常的,抛出异常导致无法解锁,所以我们还需对其进行修改

static Singleton* GetInstance()
		{
			//::Sleep(1000);  增加没加锁时出现线程不安全的条件(2个以上线程同时过了判断条件)

			// 双检查
			if (_pinst == nullptr)
			{
				//_mtx.lock();
				unique_lock<mutex> lock(_mtx);
				if (_pinst == nullptr)
				{
					_pinst = new Singleton;
				}
				//_mtx.unlock();
			}

			// ...

			return _pinst;
		}
        static void DelInstance()//这个函数也可以不加,对象在生命周期结束也会自动释放
		{
			//unique_lock<mutex> lock(_mtx);
			delete _pinst;
			_pinst = nullptr;
		}

		Singleton(const Singleton& s) = delete;

	private:
		Singleton()
		{}

		static Singleton* _pinst;
		static mutex _mtx;
	};

我们加上智能锁守卫,使其解锁一定会被执行,而我们在外部还加了一个判断,这就是为了双重保险,直接阻止多个线程进入锁中的情况,我们开始的两个进程,一个堵在了unique_lock,一个进去创建对象,当进行创建过后,我们就不需要加锁了,所以在给外层加个if,进行优化

这其实就是我们单例模式中的懒汉模式,当我们走到创建对象的时候,为空,我们才开始创建对象,第一次获取对象时,创建对象

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

下面我们来看另一个单例模式,饿汉模式

// 饿汉模式  一开始(main函数之前)就创建对象
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			return &_inst;
		}

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

		static Singleton _inst;
	};

	Singleton Singleton::_inst;
	// static对象是在main函数之前创建的,这会只有主线程,所以不存在线程安全。

我们饿汉模式,指的是在一开始就创建了对象,所以不存在线程安全问题

懒汉模式与饿汉模式的对比

 1、懒汉模式需要考虑线程安全和释放的问题,实现相对更复杂,饿汉模式不存在以上问题,实现简单
 2、懒汉是一种懒加载模式需要时在初始化创建对象,不会影响程序的启动。饿汉模式则相反,程序启动阶段就创建初始化实力对象,会导致程序启动慢,影响体验。
 3、如果有多个单例类,假设有依赖关系(B依赖A),要求A单例先创建初始化,B单例再创建初始化,那么就不能饿汉,因为无法保证创建初始化顺序,这时用懒汉我们就可以手动控制。

 总结一下:实际中懒汉模式还是更实用一些

上一篇:Java基础笔记32——单例模式


下一篇:Java 设计模式-单例模式 理论代码相结合,springmvc教程下载