【C++】—— 单例模式详解

这篇具有很好参考价值的文章主要介绍了【C++】—— 单例模式详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言:

  • 本期,我将要讲解的是有关C++中常见的设计模式之单例模式的相关知识!!

目录

(一)设计模式的六⼤原则

(二)设计模式的分类

(三)单例模式

1、定义

 2、实现方式

1️⃣ 懒汉模式

 2️⃣ 饿汉模式

(四)懒汉模式的安全实现

总结


首先我们需要知道的是设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路。它不是语法规定,⽽是⼀套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。
 


(一)设计模式的六⼤原则

单⼀职责原则(Single Responsibility Principle)

  • 类的职责应该单⼀,⼀个⽅法只做⼀件事。职责划分清晰了,每次改动到最⼩单位的⽅法或类。
  • 使用建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很⾼的函数、数据的封装
  • 用例:⽹络聊天:⽹络通信&聊天,应该分割成为⽹络通信类&聊天类
     

开闭原则(Open Closed Principle)

  • 对扩展开放,对修改封闭
  • 使用建议:对软件实体的改动,最好⽤扩展⽽⾮修改的⽅式。
  • 用例:超时卖货:商品价格---不是修改商品的原来价格,⽽是新增促销价格。

⾥⽒替换原则(Liskov Substitution Principle)

  • 通俗点讲,就是只要⽗类能出现的地⽅,⼦类就可以出现,⽽且替换为⼦类也不会产⽣任何错误或异常。
  • 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意⽗类的protected⽅法,⼦类尽量不要暴露⾃⼰的public⽅法供外界调⽤。
  • 使用建议:⼦类必须完全实现⽗类的⽅法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法时,输⼊参数可以被放⼤,输出可以缩⼩
  • 用例:跑步运动员类 -- 会跑步,⼦类⻓跑运动员 -- 会跑步且擅⻓⻓跑,⼦类短跑运动员-会跑步且擅⻓短跑

依赖倒置原则(Dependence Inversion Principle)

  • ⾼层模块不应该依赖低层模块,两者都应该依赖其抽象。不可分割的原⼦逻辑就是低层模式,原⼦逻辑组装成的就是⾼层模块。
  • 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖
  • 使用建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅法。结合⾥⽒替换原则使⽤。
  • 用例:奔驰⻋司机类--只能开奔驰;司机类--给什么⻋,就开什么⻋;开⻋的⼈:司机--依赖于抽象

迪⽶特法则(Law of Demeter),⼜叫“最少知道法则”

  • 尽量减少对象之间的交互,从⽽减⼩类之间的耦合。⼀个对象应该对其他对象有最少的了解。
  • 对类的低耦合提出了明确的要求:只和直接的朋友交流,朋友之间也是有距离的。⾃⼰的就是⾃⼰的(如果⼀个⽅法放在本类中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)
  • 用例:⽼师让班⻓点名--⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点名,⽼师勾选

接⼝隔离原则(Interface Segregation Principle)

  • 客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上
  • 使用建议:接⼝设计尽量精简单⼀,但是不要对外暴露没有实际意义的接⼝。
  • 用例:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴露数据库操作

从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,⽤抽象构建框架,⽤实现扩展细节,具体到每⼀条设计原则,则对应⼀条注意事项:

  • 单⼀职责原则告诉我们实现类要职责单⼀;
  • ⾥⽒替换原则告诉我们不要破坏继承体系;
  • 依赖倒置原则告诉我们要⾯向接⼝编程;
  • 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀;
  • 迪⽶特法则告诉我们要降低耦合;
  • 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。

(二)设计模式的分类

设计模式可以根据其目的和使用方式进行分类。以下是常见的设计模式分类:

  1. 创建型模式(Creational Patterns):这些模式关注对象的创建过程,用于实例化对象的方式。常见的创建型模式包括:

    • 单例模式(Singleton Pattern)
    • 工厂模式(Factory Pattern)
    • 抽象工厂模式(Abstract Factory Pattern)
    • 建造者模式(Builder Pattern)
    • 原型模式(Prototype Pattern)
  2. 结构型模式(Structural Patterns):这些模式关注对象之间的组合和关联方式,以形成更大的结构。常见的结构型模式包括:

    • 适配器模式(Adapter Pattern)
    • 装饰器模式(Decorator Pattern)
    • 代理模式(Proxy Pattern)
    • 桥接模式(Bridge Pattern)
    • 组合模式(Composite Pattern)
    • 外观模式(Facade Pattern)
    • 享元模式(Flyweight Pattern)
  3. 行为型模式(Behavioral Patterns):这些模式关注对象之间的通信和交互方式,以定义对象之间的责任分配和行为。常见的行为型模式包括:

    • 观察者模式(Observer Pattern)
    • 策略模式(Strategy Pattern)
    • 模板方法模式(Template Method Pattern)
    • 命令模式(Command Pattern)
    • 迭代器模式(Iterator Pattern)
    • 状态模式(State Pattern)
    • 职责链模式(Chain of Responsibility Pattern)
    • 中介者模式(Mediator Pattern)
    • 访问者模式(Visitor Pattern)
    • 备忘录模式(Memento Pattern)
    • 解释器模式(Interpreter Pattern)

除了这些主要的分类,其实还有:并发模式和线程池模式


(三)单例模式

本期,我们先学习设计模式中的第一种模式——单例模式

1、定义

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


 2、实现方式

单例模式通常有两种模式,分别为懒汉式单例饿汉式单例。两种模式实现方式分别如下:
 

1️⃣ 懒汉模式
 

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

对于懒汉模式常见的有两种设计方法:

  • a. 静态指针 + 用到时初始化
  • b. 局部静态变量
     

(1)懒汉模式实现一:静态指针 + 用到时初始化
 

template<typename T>
class Singleton
{
public:
	static T& getInstance()
	{
		if (!_value)
		{
			_value = new T();
		}
		return *_value;
	}

private:

	Singleton() 
	{}

	~Singleton()
	{}

	static T* _value;
};

template<typename T>
T* Singleton<T>::_value = NULL;

【解释说明】

单线程中,这样的写法是可以正确使用的,但是在多线程中就不行了,该方法是线程不安全的。

  • a. 假如线程A和线程B, 这两个线程要访问getInstance函数,线程A进入getInstance函数,并检测if条件,由于是第一次进入,value为空,if条件成立,准备创建对象实例。
  • b. 但是,线程A有可能被OS的调度器中断而挂起睡眠,而将控制权交给线程B。
  • c. 线程B同样来到if条件,发现value还是为NULL,因为线程A还没来得及构造它就已经被中断了。此时假设线程B完成了对象的创建,并顺利的返回。
  • d. 之后线程A被唤醒,继续执行new再次创建对象,这样一来,两个线程就构建两个对象实例,这就破坏了唯一性

另外,还存在内存泄漏的问题,new出来的东西始终没有释放,下面是一种饿汉式的一种改进。
 

template<typename T>
class Singleton
{
public:
	static T& getInstance()
	{
		if (!_value)
		{
			_value = new T();
		}
		return *_value;
	}

private:
    // 实现一个内嵌垃圾回收类
	class CGarbo
	{
	public:
		~CGarbo()
		{
			if (Singleton::_value)
				delete Singleton::_value;
		}
	};

	static CGarbo Garbo;

	Singleton()
	{};

	~Singleton()
	{};

	static T* _value;
};

template<typename T>
T* Singleton<T>::_value = nullptr;

【解释说明】

  • 在程序运行结束时,系统会调用 Singleton 的静态成员Garbo的析构函数,该析构函数会删除单例的唯一实例。

使用这种方法释放单例对象有以下特征:

  1. 在单例类内部定义专有的嵌套类
  2. 在单例类内定义私有的专门用于释放的静态成员;
  3. 利用程序在结束时析构全局变量的特性,选择最终的释放时机
     

【注意】

  1. 需要注意的是,该代码在多线程环境下并不是线程安全的;
  2. 如果多个线程同时用 getInstance(),有可能会创建多个对象。为了实现线程安全的单例模式,需要使用适当的同步机制,例如使用互斥锁或双重检查锁定这个下面会讲到

(2)懒汉模式实现二:局部静态变量
 

template<typename T>
class Singleton {
public:
    static T& getInstance() {
        static T instance;
        return instance;
    }

private:
    Singleton() {}
    ~Singleton() {}

    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
};

【解释说明】

  1. 在上述代码中将拷贝构造函数和赋值操作符的声明放在了私有部分,但没有给它们提供定义。这样,任何尝试在类外部调用这些函数的操作都会导致链接错误。这种方式达到了限制拷贝的目的,确保单例对象的唯一性。
  2. 使用此模式时,通过Singleton<YourClass>::getInstance() 调用 getInstance() 函数来获取到 YourClass 类的单例对象。
  3. 这种实现方式也是一种常见的懒汉模式实现方法,不需要使用 delete 关键字,而是通过私有化拷贝构造函数和赋值操作符来限制拷贝行为,从而实现单例的唯一性。

 2️⃣ 饿汉模式

程序启动时就会创建⼀个唯⼀的实例对象。因为单例对象已经确定,所以⽐较适⽤于多
线程环境中
,多线程获取单例对象不需要加锁,可以有效的避免资源竞争,提⾼性能。

(1)饿汉模式实现一:直接定义静态对象
 

template<typename T>
class Singleton 
{
private:
	static T _eton;
private:
	Singleton() {}
	~Singleton() {}

public:
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	static T& getInstance()
	{
		return _eton;
	}
};

template<typename T>
T Singleton<T>::_eton;

优点:

  • 实现简单,多线程安全。

缺点:

  • a. 如果存在多个单例对象且这几个单例对象相互依赖,可能会出现程序崩溃的危险。原因:对编译器来说,静态成员变量的初始化顺序和析构顺序是一个未定义的行为;
  • b. 在程序开始时,就创建类的实例,如果Singleton对象产生很昂贵,而本身有很少使用,这种方式单从资源利用效率的角度来讲,比懒汉式单例类稍差些。但从反应时间角度来讲,则比懒汉式单例类稍好些。


使用条件:

  • a. 当肯定不会有构造和析构依赖关系的情况。
  • b. 想避免频繁加锁时的性能消耗

(2)饿汉模式实现二:静态指针 + 类外初始化时new空间实现
 

template<typename T>
class Singleton
{
private:
    static T* _eton;

    Singleton() {}
    ~Singleton() {}

public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static T& getInstance()
    {
        return *_eton;
    }
};

template<typename T>
T* Singleton<T>::_eton = new T();

【小结】

  1. 需要注意的是,由于饿汉模式在程序启动时就创建了单例对象,因此无法实现延迟加载的效果;
  2. 如果在程序运行过程中不一定需要使用该单例对象,会导致不必要的资源消耗;
  3. 因此,懒汉模式通常更常用,只在需要时才创建单例对象。

(四)懒汉模式的安全实现



class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 双检查加锁
		if (m_pInstance == nullptr) {
			m_mutex.lock();
			if (m_pInstance == nullptr)
			{
				m_pInstance = new Singleton;
			}
			m_mutex.unlock();
		}
		return m_pInstance;
	}

	static void DelInstance()
	{
		m_mutex.lock();
		if (m_pInstance)
		{
			delete m_pInstance;
			m_pInstance = nullptr;
		}
		m_mutex.unlock();
	}

	// 实现一个内嵌垃圾回收类
	class CGarbo
	{
	public:
		~CGarbo()
		{
			DelInstance();
		}
	};
	// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
	static CGarbo Garbo;

	// 一般全局都要使用单例对象,所以单例对象一般不需要显示释放
	// 有些特殊场景,想显示释放一下
	void Add(const string& str)
	{
		_vmtx.lock();

		_v.push_back(str);

		_vmtx.unlock();
	}

	void Print()
	{
		_vmtx.lock();

		for (auto& e : _v)
		{
			cout << e << endl;
		}
		cout << endl;

		_vmtx.unlock();
	}

	~Singleton()
	{
		// 持久化
		// 比如要求程序结束时,将数据写到文件,单例对象析构时持久化就比较好
	}
private:
	mutex _vmtx;
	vector<string> _v;

private:
	// 构造函数私有
	Singleton()
	{}

	// 防拷贝
	//Singleton(Singleton const&);
	//Singleton& operator = (Singleton const&);

	static mutex m_mutex;			//互斥锁
	static Singleton* m_pInstance; // 单例对象指针
};

Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_mutex;

int main()
{
	srand(time(0));

	int n = 3000;
	thread t1([n]() {
		for (size_t i = 0; i < n; ++i)
		{
			Singleton::GetInstance()->Add("t1线程:" + to_string(rand()));
		}
		});

	thread t2([n]() {
		for (size_t i = 0; i < n; ++i)
		{
			Singleton::GetInstance()->Add("t2线程:" + to_string(rand()));
		}
		});

	t1.join();
	t2.join();

	Singleton::GetInstance()->Print();

	return 0;
}


总结

到此,关于单例模式的讲解便到此结束了。接下来,简单的回顾总结一下本文!!!

懒汉模式和饿汉模式都是单例模式的实现方式,都用于确保一个类只有一个实例,并提供全局访问点。

  • 懒汉模式:是指在首次使用时才创建对象实例。具体实现上,懒汉模式通常通过延迟加载的方式,在getInstance()方法中进行判断,如果实例尚未创建,则在需要时才创建并返回。懒汉模式的优点是节省了资源,只有在需要时才会创建实例,但缺点是在多线程环境下需要额外的同步机制,以确保线程安全。
  • 饿汉模式:是指在类加载时即创建对象实例。具体实现上,饿汉模式通过在类的静态成员变量中直接创建实例对象,并在getInstance()方法中返回该实例。饿汉模式的优点是实现简单,不需要考虑多线程同步的问题,但缺点是在应用程序启动时就会创建实例,可能会浪费一些资源。

在实际应用中根据具体情况权衡懒汉模式和饿汉模式的优缺点,并选择适合的实现方式。

以上便是本文的全部内容,感谢大家的观看和支持!!!

【C++】—— 单例模式详解,C++学习,c++,开发语言文章来源地址https://www.toymoban.com/news/detail-698507.html

到了这里,关于【C++】—— 单例模式详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【单例设计模式原理详解】Java/JS/Go/Python/TS不同语言实现

    单例模式(Singleton Pattern)属于创建型设计模式,这种模式只创建一个单一的类,保证一个类只有一个实例,并提供一个访问该实例的全局节点。 当您想控制实例数目,节省系统资源,并不想混用的时候,可以使用单例模式。单例有很多种实现方式,主要分为懒汉和饿汉模式

    2023年04月27日
    浏览(93)
  • 【Unity3D日常开发】Unity3D中实现单例模式详解

    推荐阅读 CSDN主页 GitHub开源地址 Unity3D插件分享 简书地址 我的个人博客 大家好,我是佛系工程师 ☆恬静的小魔龙☆ ,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 首先,说一下,什么是 单例模式(Singleton) 。 单例模式是设计模式中常见的一种设计模式,目的是为了

    2024年02月02日
    浏览(65)
  • 【C++】设计模式-单例模式

    目录 一、单例模式 单例模式的三个要点 针对上述三要点的解决方案 常用的两类单例模式  二、懒汉模式实现 1.基本实现 2.锁+静态成员析构单例 3.双层检查锁定优化 4.双层检查锁定+智能指针 三、饿汉模式实现 1.基础实现 2.嵌套内部类解决内存泄漏 3.智能指针解决内存泄漏

    2024年02月16日
    浏览(38)
  • C++设计模式:单例模式(十)

    1、单例设计模式 单例设计模式,使用的频率比较高,整个项目中某个特殊的类对象只能创建一个 并且该类只对外暴露一个public方法用来获得这个对象。 单例设计模式又分懒汉式和饿汉式,同时对于懒汉式在多线程并发的情况下存在线程安全问题 饿汉式:类加载的准备阶段

    2024年04月14日
    浏览(47)
  • C++设计模式代码--单例模式

    参考:5. 单例模式(Singleton) (yuque.com) 1、什么是单例模式 保证一个类只有一个实例,并提供一个访问该实例的全局节点; 2、什么情况下需要单例模式 某个类的对象在软件运行之初就创建,并且在软件的很多地方都需要读写这个类的信息;使用单例模式的话,类对象就只要

    2024年02月03日
    浏览(64)
  • C++的单例模式

    忘记之前有没有写过单例模式了。 再记录一下: 我使用的代码: 双锁单例: 单例模式的不同实现方式各有优缺点 双检锁(Double Checked Locking): 优点: 线程安全。 在实例已经被创建之后,直接返回实例,避免了每次获取实例时都需要获取锁的开销。 缺点: 代码相对复杂

    2024年02月10日
    浏览(46)
  • C++ 单例模式

    C++ 单例模式跟Java中的单例模式没什么区别 什么是单例?  单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例 什么时候使用单例 1个类里面的方法要在很多地方都使用到的时候建议使用单例。  单例的3个步骤,也是判断是否是单例的依据 1 就是

    2024年02月05日
    浏览(28)
  • 单例模式——C++版本

    在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一实例向其他模块提供数据的全局访问,这种模式就叫单例模式。 类中多对象的操作函数有如下几个: 构造函数 : 能够创建出一个新对象; 拷贝构造函数 :能够根据一个已经存在的对象拷贝出一个新对

    2024年02月09日
    浏览(34)
  • 单例模式(C++)

    保证一个类仅有一个实例,并提供一个该实例的全局访问点。 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率。 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例? 这应该是类设

    2024年02月13日
    浏览(29)
  • 【C++基础】单例模式

    本文章参考:单例模式 - 巴基速递 | 爱编程的大丙 什么是单例模式 在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一实例向其他模块提供数据的全局访问,这种模式就叫单例模式。单例模式的典型应用就是任务队列。 如果使用单例模式,首先要保证这

    2024年02月07日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包