C++ 设计模式(单例+工厂)

C++ 设计模式(单例+工厂)


更好的阅读体验:【前往作者个人站点 - AriesfunのBlog】
文章内容若有疑问或错误,欢迎交流、指正,互相学习哈。


1.单例模式

推荐一个讲解单例视频,整体概述比较全面:【视频 C++单例模式:从设计到实现】

工作具体项目中的应用,可以看看这个视频,讲了单例的各种用法:【视频 C++单例模式】

下面是我自己从上面视频中的整理的一些讲解的理解和代码实现,主要是懒汉式单例模式。

(1).概念

通过单例模式的方法创建的类在当前进程中有且仅有一个实例。单例模式,属于创建类型的一种常用的软件设计模式

具体细分:

  • 懒汉式单例,会提供一个创建对象的方法(需要使用时创建)

  • 饿汉式单例,在类加载的时候就创建对象(程序运行时创建,比较着急)

共同点:

01.要声明一个静态的类引用变量

02.类的构造函数要私有

03.提供一个公有的创建对象的方法,能够全局访问

区别:

懒汉式单例是在方法调用时创建对象,而饿汉式是在类加载是创建对象;

多线程情况下,懒汉式单例存在线程安全问题,饿汉式不存在线程安全问题。


(2).应用场景

对程序运行期间对全局唯一资源的统一访问。

比如,配置管理、日志记录、线程池、连接池、内存池、对象池、消息队列等。

自己最近学习的tcp服务端,就用到了单例模式(懒汉式),这种写法是使用局部静态变量方式(与进程生命周期同步);

可以避免进行释放内存操作,这种单例模式的写法比较推荐;

这是因为静态局部变量在C++11标准之后的编译器实现中会进行线程安全的初始化,保证局部变量初始化严格发生一次。

在初始化过程中,编译器会确保只有一个线程能够执行该静态局部变量的初始化代码,从而避免了多线程竞争的问题。

具体的说明,可以看这篇文档,【文档 静态局部变量】

//通过静态成员变量实现单例
//懒汉式 (多数情况下,这种单例模式是安全的)
class Singleton
{
private:
    Singleton(){}
    Singleton(const Singleton &) = delete;
    Singleton &operator=(const Singleton &) = delete;
public:
    static Singleton &GetInst()
    {
        static Singleton single;
        return single;
    }
}; 

具体项目中应用的例子,

image-20230807084123870
(3).实现单例模式(懒汉式较常用)

在大的实际项目中可能有多个类用到了单例,不可能把他们单独放在一起集中来初始化,所以多用懒汉式单例,具体使用时来创建单例。

  • 实现步骤

    • step1.将类的构造方法定义为私有。
    • step2.定义一个私有类的静态实例。
    • step3.提供一个公有的获取实例的静态方法。
  • 涉及的知识点

    static静态成员变量,fiend友元类,template模板类

(3-1).单例模式实现(懒汉+饿汉)

懒汉式单例,比较常用

代码实现:a.h

#pragma once
#include <iostream>
#include <string>

class A{

private: // step1.让类的构造函数私有(或受保护)
    A() : m_name("A") {}
    A(const A &other) {}
    ~A() {}
    A& operator = (const A &other);

private: // step2.定义一个私有静态类的实例
    static A* m_instance;
    std::string m_name;

public: // step3.提供公有的获取实例的静态方法
    static A* instance() {
        if(m_instance == nullptr) {
            m_instance = new A();
        }
        return m_instance;
    }
    void show() {
        std::cout << "m_name: " << m_name << std::endl;
    }
};

A* A::m_instance = nullptr; // 静态变量初始化

主程序,main.cpp

#include <iostream>
#include "a.h"
// #include "b.h" // 可仿照"a.h"可以写一个B类来实现单例模式

/*
单例模式:保证全局有且仅有一个实例
*/
int main() {
    // 编写两个头文件让A类,B类都实现单例
    A* a1 = A::instance();
    a1->show();
    cout << "a1: " << a1 << endl;  
    auto a2 = A::instance();
    a2->show();
    cout << "a2: " << a2 << endl; // a1,a2都指向同一地址(表示是同一实例)
    
	return 0;
}

代码运行结果,

m_name: A
a1: 0x55b10376d270
m_name: A
a2: 0x55b10376d270

饿汉式单例(不常用),是线程安全的

代码实现:Singleton.h

# pragma once
class Singleton {

private:
    Singleton() {} // step1.将类的构造方法定义为私有。
    // 定义一个私有类的静态实例。(饿汉式单例类的实例会在类被加载时就创建好,多线程下是安全的)
    static Singleton* instance;

public:
    static Singleton* getSingleton() { // 提供一个公有的获取实例的静态方法。
        return instance;
    }
};

Singleton* Singleton::instance = new Singleton(); // 在类外部进行初始化,确保了实例只会在程序启动时被创建一次

具体使用,main.cpp

#include <iostream>
#include "Singleton.h"

int main() {

    // 单例模式测试
    Singleton* a1 = Singleton::getSingleton();
    Singleton* a2 = Singleton::getSingleton();
    std::cout << "a1's addr: " << a1 << std::endl;
    std::cout << "a2's addr: " << a2 << std::endl;
    return 0;
}

运行结果如下,

a1's addr: 0x6c1780
a2's addr: 0x6c1780

(3-2).用类模板优化写法(懒汉式)

定义一个类模板,singleton.h

#pragma once
namespace ariesfun {
namespace utility {
	// 使用模板类来实现单例模式
    template <typename T> 
    class Singleton {

    public: // 提供公有访问实例的方法
        static T* instance() // 多线程环境下,可能会出安全问题
        {
            if(m_instance == nullptr) {
                m_instance = new T(); // 这里要访问对应私有类的构造函数,需要将当前类在类A中声明为友元类
            }
            return m_instance; // 返回全局实例的指针
        }
    
    private:
        Singleton() {}
        Singleton(const Singleton<T> &another);
        ~Singleton() {}
        Singleton<T>& operator = (const Singleton<T> &another);
        
    private:
        static T* m_instance;
    };

    // 注意模板类的static成员要放在.h文件里初始化
    template <typename T>
    T* Singleton<T>::m_instance = nullptr; // 模板类的静态成员初始化
}
}

改写a.h

#pragma once
#include <iostream>
#include <string>

#include "singleton.h" // 引入头文件和对应作用域
using namespace ariesfun::utility;

class A{

private: // step1.让类的构造函数私有(或受保护)
    A() : m_name("A") {}
    A(const A &other) {}
    ~A() {}
    A& operator = (const A &other);

private: // step2.定义一个私有静态类的实例
    // static A* m_instance; (在类模板中已经实现)
    std::string m_name;

public: // step3.提供公有的获取实例的静态方法
    // static A* instance() {...} (在类模板中已经实现)
    void show() {
    	std::cout << "m_name: " << m_name << std::endl;
    }
    friend class Singleton<A>; // 让Singleton能访问A的私有成员
};

// A* A::m_instance = nullptr; // 静态变量初始化

新的main.cpp

#include <iostream>
#include "a.h"
// #include "b.h"
using namespace std;

#include "singleton.h"
using namespace ariesfun::utility;

int main() {    
    // 编写类模板实现单例
    auto sa1 = Singleton<A>::instance();
    sa1->show();
    cout << "sa1: " << sa1 << endl;

    auto sa2 = Singleton<A>::instance();
    sa2->show();
    cout << "sa2: " << sa2 << endl;
    return 0;
}

代码运行结果,

m_name: A
sa1: 0x5568f0b07270
m_name: A
sa2: 0x5568f0b07270

(3-3).其他拓展

你可能见到过有些官方源码还有其他人,写法是宏定义或者在使用默认构造、拷贝、赋值后,加上了一些关键字(defalut,delete),这是一种优化的写法。

比如,

#define SINGLETON(classname)                                    \
    friend class Singleton<classname>;                          \
    private:                                                    \
        classname() = default;                                  \ // 使用默认构造
        classname(const classname &) = delete;                  \ // 禁用拷贝构造
        classname & operator = (const classname &) = delete;    \ // 禁用赋值操作符
        ~classname() = default									  // 使用默认析构

(1).private:: 这是一个访问权限标识符,表示以下成员都将是私有的,只能在类的内部访问。

(2).classname() = default;: 表示使用编译器生成默认的构造函数实现。在单例模式中,通常会将构造函数设为私有,以防止从外部直接创建类的实例。

(3).classname(const classname &) = delete;: 使用 = delete 表示禁用拷贝构造函数。这样做是为了防止通过拷贝构造函数创建多个实例,从而维护单例的唯一性。

(4).classname & operator=(const classname &) = delete;: 使用 = delete 表示禁用赋值操作符。这也是为了避免通过赋值操作创建多个实例。

(5).~classname() = default;: 使用 = default 表示使用编译器生成默认的析构函数实现。在单例模式中,通常不需要特殊的析构函数逻辑。

这段代码的目的是创建一个单例模式的类,其中通过私有化构造函数、拷贝构造函数和赋值操作符,以及声明友元类,来确保只有 Singleton 类能够创建和管理classname 类的唯一实例。

使用 = default= delete 是一种简洁的方式来指定默认的函数实现或禁用特定的函数。


(4).考虑多线程下的安全问题(懒汉式)

最佳解决是直接规避掉这个问题,而不是找方法去解决这个问题(加锁等)。 乱码哥牛啊,哈哈

其他加锁的方法,实际有需求或遇到线程安全问题,可以参考一下这篇博客介绍的解决方案:文档 再谈单例模式

乱码哥建议,可在main函数中这样使用,

// 解决多线程下的安全问题,可以使用如下的方案(来规避安全问题)

// 主线程中 (创建实例)
Singleton<A>::instance();
Singleton<B>::instance();

// 子线程中 (使用)
Singleton<A>::instance()->show();
Singleton<B>::instance()->show();

2.工厂模式

(1).概念

工厂模式:原来是要自己创建对象,现在是不需要自己创建对象,而是创建这个对象的工厂。

工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式。

百度百科-工厂模式就相当于创建实例对象的new,我们经常要根据类class生成实例对象,

A a=new A();工厂模式也是用来创建实例对象的,所以以后new时就要多个心眼,是否可以考虑使用工厂模式,

虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。


(2).应用场景

工厂模式的主要目的,是封装对象的创建过程,提供更高层次的抽象,降低代码之间的耦合度,使代码更加可扩展和可维护。

让我们来学习一下ChatGpt的回答,讲得倒是很全面。

image-20230808180910381
(3).实现工厂模式

下面用创建一个车的工厂来举例,

先定义两个父类的头文件,车类和车工厂类,并尝试用CMake管理,采用多文件编写。

项目目录如下,

.
├── CMakeLists.txt
├── docs
├── include
│   ├── Baoma.h
│   ├── BaomaFactory.h
│   ├── Benchi.h
│   ├── BenchiFactory.h
│   ├── Car.h
│   ├── CarFactory.h
│   └── singleton.h  # 可以忽略,这是测试单例模式时使用的类
└── src
    ├── Baoma.cpp
    ├── BaomaFactory.cpp
    ├── Benchi.cpp
    ├── BenchiFactory.cpp
    └── main.cpp

Car.cpp

#ifndef FACTORY_PATTERN_CAR_H
#define FACTORY_PATTERN_CAR_H

#include <string>
class Car {

public:
    virtual std::string get_name() = 0; // 定义虚函数,让子类来实现
    Car() = default;
};

#endif //FACTORY_PATTERN_CAR_H

CarFactory.h

#ifndef FACTORY_PATTERN_CARFACTORY_H
#define FACTORY_PATTERN_CARFACTORY_H

#include "Car.h"
class CarFactory {

public:
    virtual Car* getCar() = 0;

};

#endif //FACTORY_PATTERN_CARFACTORY_H

当我们需要创建一个车对象(奔驰车)时,我们需要创建这个车和对于这个车工厂(继承各自父类),当我们要使用这个车时,直接从它工厂里拿就行。

下面以创建奔驰车对象为例,

奔驰车类Benchi.h

#ifndef FACTORY_PATTERN_BENCHI_H
#define FACTORY_PATTERN_BENCHI_H

#include "Car.h"
class Benchi :public Car {

public:
    std::string get_name() override; // 加override,可帮助编译器检查是否正确地重写了父类的虚函数
};


#endif //FACTORY_PATTERN_BENCHI_H

// 在对应的源文件里写函数声明的实现
"Benchi.cpp"
#include "Benchi.h"

std::string Benchi::get_name() {
    return "benchi";
}    

奔驰车工厂,BenchiFactory.h

#ifndef FACTORY_PATTERN_BENCHIFACTORY_H
#define FACTORY_PATTERN_BENCHIFACTORY_H

#include "CarFactory.h"

class BenchiFactory :public CarFactory {

public:
    Car* getCar() override;
};

#endif //FACTORY_PATTERN_BENCHIFACTORY_H

// 在对应的源文件里写函数声明的实现
"BenchiFactory.cpp"
#include "BenchiFactory.h"
#include "Benchi.h"

Car* BenchiFactory::getCar() {
    return new Benchi();
}    

编写测试类,main.cpp

#include <iostream>

#include "Car.h"
// #include "Benchi.h"
// #include "BenchiFactory.h"
#include "Baoma.h"
#include "BaomaFactory.h"

int main() {

    /*
    // 原来创建对象
    Car* c = new Benchi();
    std::cout << "Car name: " << c->get_name() << std::endl; // Car name: benchi
    */

    // 工厂模式测试
    // 使用工厂来创建一辆奔驰车
    Car* c1 = (new BenchiFactory)->getCar();
    std::cout << "Car1 name: " << c1->get_name() << std::endl; // Car1 name: benchi

    // 同样,我们使用工厂来创建一辆宝马车(写法与奔驰车类似)
    // Car* c2 = (new BaomaFactory)->getCar();
    // std::cout << "Car2 name: " << c2->get_name() << std::endl; // Car2 name: baoma
    return 0;
}

参考资料

单例模式:

【文档 单例模式】

【文档 再谈单例模式】

【视频 C++单例模式:从设计到实现】

【视频 C++单例模式】

【视频 C++单例模式总结】

【视频 Java单例设计模式】

工厂模式:

【视频 C/C++项目实战-前置知识】

【仓库代码 SYaoJun/SystemProgramming】


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值