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


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

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

    	HeapOnly ho;
    	HeapOnly* p = new HeapOnly;
    
    • 1
    • 2

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

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

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

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

    🌿 静态的成员函数

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

    class HeapOnly
    {
    public:
    	static HeapOnly* CreateObj()
    	{
    		return new HeapOnly;
    	}
    
    private:
    	HeapOnly() 
    	{}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

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

    HeapOnly* p = HeapOnly::CreateObj();
    HeapOnly copy(*p);
    
    • 1
    • 2

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

    class HeapOnly
    {
    public:
    	static HeapOnly* CreateObj()
    	{
    		return new HeapOnly;
    	}
        
        //C++11 提供 delete
    	HeapOnly(const HeapOnly&) = delete;
    
    private:
    	HeapOnly() 
    	{}
    	//私有化,只声明不实现(可以实现但没必要)
    	//C++98 -- 防止拷贝
    	HeapOnly(const HeapOnly& );
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    方法1:私有化构造函数

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

    class StackOnly
    {
    public:
    	static StackOnly CreateObj()
    	{
    		return StackOnly();
    	}
    
    private:
    	StackOnly() {}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    方法2:屏蔽new

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

    98私有化

    class StackOnly
    {
    public:
    	StackOnly() {};
    private:
        void* operator new(size_t size);
    	void operator delete(void* p);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    11delete

    class StackOnly
    {
    public:
    	StackOnly() {};
    	void* operator new(size_t size) = delete;
    	void operator delete(void* p) = delete;
    private:
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

    98私有化

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

    class CopyLimit
    {
    public:
    private:
         CopyLimit(const CopyLimit&);
    	CopyLimit& operator=(const CopyLimit&);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    11delete

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

    class CopyLimit
    {
    public:
    
    	CopyLimit(const CopyLimit&) = delete;
    	CopyLimit& operator=(const CopyLimit&) = delete;
    private:
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    98私有化

    class NonInherit
    {
    public:
    	static NonInherit GetInstance()
    	{
    		return NonInherit();
    	}
    private:
    	NonInherit(){}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    11final

    class NonInherit final 
    {
    public:
    private:
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这个是继承就报错

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

    设计模式

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

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

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

    单例模式

    什么是单例模式?

    image-20220516205253203

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

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

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

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

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

    image-20220516205753351

    优点缺点
    你可以保证一个类只有一个实例违反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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

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

    懒汉模式

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

    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;
    	};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    线程安全

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

    #ifdef _WIN32
    // windows 提供多线程api 
    #else
    // linux pthread
    #endif // _WIN32
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    image-20220516214655984

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

    		static Singleton* GetInstance()
    		{
    			_mutex.lock();
    			if (_inst==nullptr)
    			{
    				_inst = new Singleton;
    			}
    			_mutex.unlock();
    
    			return _inst;
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    双检查加锁

    这个GetInstance还有没有问题?

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

    我们可以使用双检查加锁

    		static Singleton* GetInstance()
    		{
    			if (_inst==nullptr)
    			{
    				_mutex.lock();
    				if (_inst == nullptr)
    				{
    					_inst = new Singleton;
    				}
    				_mutex.unlock();
    			}
    
    			return _inst;
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

    单例释放

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

    		static void DelInstance()
    		{
    			_mutex.lock();
    			if (_inst)
    			{
    				delete _inst;
    				_inst = nullptr;
    			}
    			_mutex.unlock();
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    这个Del饿汉也适用

    垃圾回收
    		class CGarbo 
    		{
    			~CGarbo()
    			{
    				if (_inst)
    				{
    					delete _inst;
    					_inst = nullptr;
    				}
    			}
    		};
    	... 
    	static CGarbo _gc;
    	};
    	Singleton::CGarbo Singleton::_gc;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这个类的作用是,专门管理回收,我们使用内部类的方式来写,为了保证他会被调用到我们可以将他定义成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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    饿汉 V.S. 懒汉
    饿汉懒汉懒汉再优化增加描述
    优点简单,不存在线程安全问题解决了饿汉的缺点写起来方便一点,也不用加锁
    缺点1.可能会导致进程启动慢(加载类)
    2.如果有多个单例类对象实例启动顺序不确定。
    相对饿汉,复杂了一点点对象在静态区,单例对象太大的时候,不合适
    在静态区是无法主动释放的
    如果有多个单例对象,且对象间有相互依赖的初始化关系,饿汉会产生问题

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

  • 相关阅读:
    list的模拟实现
    harbor 安装
    视频知识点(19)- YUV420好,还是YUV444好?
    89.(cesium之家)cesium聚合图(自定义图片)
    IP地址规划设计
    Vue3理解(9)
    ASP.NET Core 6框架揭秘实例演示[26]:跟踪应用接收的每一次请求
    关于CUDA+Torch+TorchVision+Python环境配置问题
    2022/11/27[指针] 指针与函数基础
    压缩感知的概述梳理(4)
  • 原文地址:https://blog.csdn.net/Allen9012/article/details/125624046