[请回答C++] 设计特殊类&单例模式

设计特殊类

    • 请设计一个类,只能在堆上创建对象
    • 请设计一个类,只能在栈上创建对象
      • 方法1:私有化构造函数
      • 方法2:屏蔽new
        • 98私有化
        • 11delete
    • 请设计一个类,不能被拷贝
      • 98私有化
      • 11delete
    • 请设计一个类,不能被继承
      • 98私有化
      • 11final
    • 请设计一个类,只能创建一个对象(单例模式)
      • 设计模式
      • 单例模式
        • 什么是单例模式?
        • 两种实现模式
          • 饿汉模式
          • 懒汉模式
            • 线程安全
            • 双检查加锁
            • 单例释放
            • 垃圾回收
          • 懒汉再优化
          • 饿汉 V.S. 懒汉

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

这样设计的目的是为了不能在栈上生成对象,通常情况下,我们既可以new也可以直接在栈上创建一个对象实例

	HeapOnly ho;
	HeapOnly* p = new HeapOnly;

将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象

既然构造函数是公有的话,那么是可以随便在栈上创建对象的,那么为了不让你随便创建对象,我可以将类的构造函数私有化,但是这样的话虽然不能再栈上创建对象,但是我也不能new对象了,那么我们怎么解决呢?

成员函数,在该函数中完成堆对象的创建

既然不能new一个对象,我们可以通过一个方法来返回一个对象,只是一个方法的话够吗?我没有对象,怎么能有一个方法呢?

静态的成员函数

为了解决这个先有鸡还是先有蛋的问题,我们就使这个函数变为静态成员函数,静态成员函数不需要创建对象也可以调用,现在就没问题了吗?能够保证只能对上构建对象了吗?

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

private:
	HeapOnly() 
	{}
};

拷贝构造私有化或者delete关键字

我通过拷贝构造还是可以栈上构建对象

HeapOnly* p = HeapOnly::CreateObj();
HeapOnly copy(*p);

于是操作拷贝构造,私有化和删除关键字二选一即可

class HeapOnly
{
public:
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
    
    //C++11 提供 delete
	HeapOnly(const HeapOnly&) = delete;

private:
	HeapOnly() 
	{}
	//私有化,只声明不实现(可以实现但没必要)
	//C++98 -- 防止拷贝
	HeapOnly(const HeapOnly& );
};

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

方法1:私有化构造函数

类似的私有化构造,但是这次不可以私有化拷贝构造了因为,return返回对象是在栈上的,返回对象是需要拷贝构造临时变量的

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

private:
	StackOnly() {}
};

方法2:屏蔽new

要么私有化要么删除,不过这个写法还有一个缺陷,可以在静态区创建对象

98私有化
class StackOnly
{
public:
	StackOnly() {};
private:
    void* operator new(size_t size);
	void operator delete(void* p);
};
11delete
class StackOnly
{
public:
	StackOnly() {};
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;
private:
};

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

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

98私有化

设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了
只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。

class CopyLimit
{
public:
private:
     CopyLimit(const CopyLimit&);
	CopyLimit& operator=(const CopyLimit&);
};

11delete

在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。

class CopyLimit
{
public:

	CopyLimit(const CopyLimit&) = delete;
	CopyLimit& operator=(const CopyLimit&) = delete;
private:
};

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

98私有化

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

把构造函数私有化,则子类不能使用父类的构造函数,但是这种方式不够彻底,是几时可以被继承的,只是限制了子类继承后不能实例化对象,也就是说实例化就报错

11final

class NonInherit final 
{
public:
private:
};

这个是继承就报错

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

设计模式

设计模式是大量在实践中总结和理论化之后优选的代码结构,编程风格,以及解决问题的思考方式

为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙武就总结出了《孙子兵法》。孙子兵法也是类似。

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

单例模式

什么是单例模式?

[请回答C++] 设计特殊类&单例模式_第1张图片

单例模式同时解决了两个问题

  1. 确保一个类只有一个实例

  2. 为该实例提供一个全局访问点

比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

政府是单例模式的一个很好的例子。一个国家只能有一个官方政府。无论组成政府的个人的个人身份如何

[请回答C++] 设计特殊类&单例模式_第2张图片

优点 缺点
你可以保证一个类只有一个实例 违反Single Responsibility Principle。该模式同时解决了两个问题
你获得了一个指向该实例的全局访问节点 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等
仅在首次请求单例对象时对其进行初始化。 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象
单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式
两种实现模式

两种单例模式的主要区别就是创建对象的时机不同

饿汉模式

就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。

// 饿汉模式 -- 程序开始main执行之前就创建单例对象
// 提供一个静态指向单例对象的成员指针,初始化时new一个对象给它
namespace hungry
{
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			return _inst;
		}
		void Print()
		{
			cout << _str << endl;
		}

	private:
		Singleton()
			:_str("")
		{}

		Singleton(const Singleton&) = delete;
		Singleton& operator=(const Singleton&) = delete;
		string _str = "言之命至";
		static Singleton* _inst;
	};
	Singleton* Singleton::_inst = new Singleton;
}

如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。

懒汉模式

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

namespace lazy
{
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			if (_inst==nullptr)
			{
				_inst = new Singleton;
			}
			return _inst;
		}
		void Print()
		{
			cout << _str << endl;
		}

	private:
		Singleton()
			:_str("")
		{}

		Singleton(const Singleton&) = delete;
		Singleton& operator=(const Singleton&) = delete;
		string _str = "言之命至";
		static Singleton* _inst;
	};
}
线程安全

但是当下情况懒汉多个线程会产生问题:比如现在来了两个线程,俩个线程都卡在判断nullptr还没有new的时候就都会去new对象,这样的话会导致慢的把快的覆盖了,所以问题很明显,需要加锁,Linux可以使用pthread库,但是Windows使用VS2019比较麻烦,Windows的互斥锁和Linux互斥锁是不一样的,传统方法是用宏来实现条件编译,可是太麻烦了

#ifdef _WIN32
// windows 提供多线程api 
#else
// linux pthread
#endif // _WIN32

因此此时C++11提供了可以跨平台使用的方法

[请回答C++] 设计特殊类&单例模式_第3张图片

修改一下实例,使其线程安全

		static Singleton* GetInstance()
		{
			_mutex.lock();
			if (_inst==nullptr)
			{
				_inst = new Singleton;
			}
			_mutex.unlock();

			return _inst;
		}
双检查加锁

这个GetInstance还有没有问题?

一般的加锁是保护一个操作执行,二这里是实际上只保护第一次进来的时候不能同时操作,只有第一次来有意义,后面来都没有意思了,加锁会引发效率低下,频繁的加锁和解锁会导致线程频繁的切入切出,

我们可以使用双检查加锁

		static Singleton* GetInstance()
		{
			if (_inst==nullptr)
			{
				_mutex.lock();
				if (_inst == nullptr)
				{
					_inst = new Singleton;
				}
				_mutex.unlock();
			}

			return _inst;
		}

第二次和之后 的加锁因为已经有对象了,所以根本不用进循环,也不会经历重复进行无意义的加锁和解锁的操作,所以双检查还是6的

单例释放

一般情况下单例是不需要释放的,程序结束的时候会自动释放,如果非要释放也不是不可以

		static void DelInstance()
		{
			_mutex.lock();
			if (_inst)
			{
				delete _inst;
				_inst = nullptr;
			}
			_mutex.unlock();
		}

这里不加双检查也是可以的,因为调用不多

这个Del饿汉也适用

垃圾回收
		class CGarbo 
		{
			~CGarbo()
			{
				if (_inst)
				{
					delete _inst;
					_inst = nullptr;
				}
			}
		};
	... 
	static CGarbo _gc;
	};
	Singleton::CGarbo Singleton::_gc;

这个类的作用是,专门管理回收,我们使用内部类的方式来写,为了保证他会被调用到我们可以将他定义成static

懒汉再优化

初始化只在第一次调用的时候初始化

// 单例对象在静态区,如果单例对象太大,不太好,不合适。
// 再挑挑刺,想主动释放单例对象,无法主动控制。
namespace lazy2
{
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			static Singleton inst;
			return &inst;
		}


	private:
		Singleton()
		{
			//假设到哪里类构造函数中,要做很多配置初始化
		}

		Singleton(const Singleton&) = delete;
		Singleton& operator=(const Singleton&) = delete;
		static std::mutex _mutex;
	};
	std::mutex Singleton::_mutex;
}
饿汉 V.S. 懒汉
饿汉 懒汉 懒汉再优化 增加描述
优点 简单,不存在线程安全问题 解决了饿汉的缺点 写起来方便一点,也不用加锁
缺点 1.可能会导致进程启动慢(加载类)
2.如果有多个单例类对象实例启动顺序不确定。
相对饿汉,复杂了一点点 对象在静态区,单例对象太大的时候,不合适
在静态区是无法主动释放的
如果有多个单例对象,且对象间有相互依赖的初始化关系,饿汉会产生问题

模式还有很多迭代器模式,适配器模式,工厂模式,观察者模式

你可能感兴趣的:(请回答C++进阶,c++,单例模式,开发语言,java)