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

1.单例模式

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

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

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

(1).概念

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

具体细分:

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

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

共同点:

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

02.类的构造函数要私有

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

区别:

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

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


(2).应用场景

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//通过静态成员变量实现单例
//懒汉式 (多数情况下,这种单例模式是安全的)
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

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
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include "a.h"
using namespace std;
// #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;
}

代码运行结果,

1
2
3
4
m_name: A
a1: 0x55b10376d270
m_name: A
a2: 0x55b10376d270

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

代码实现:Singleton.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# pragma once
class Singleton {

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

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

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

具体使用,main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#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;
}

运行结果如下,

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

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

定义一个类模板,singleton.h

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
30
31
#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

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
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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;
}

代码运行结果,

1
2
3
4
m_name: A
sa1: 0x5568f0b07270
m_name: A
sa2: 0x5568f0b07270

(3-3).其他拓展

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

比如,

1
2
3
4
5
6
7
#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函数中这样使用,

1
2
3
4
5
6
7
8
9
// 解决多线程下的安全问题,可以使用如下的方案(来规避安全问题)

// 主线程中 (创建实例)
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管理,采用多文件编写。

项目目录如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── 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

1
2
3
4
5
6
7
8
9
10
11
12
#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

1
2
3
4
5
6
7
8
9
10
11
12
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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();
}

在这个例子中,基类CarFactory定义了一个抽象的getCar函数,由子类来实现具体的创建过程。

BenchiFactory是一个具体的子类,它继承了CarFactory并实现了getCar函数,返回一个Benchi对象。

这种模式的好处是,当需要添加新的汽车品牌时,只需创建一个新的子类并实现相应的getCar函数即可,不会对

已有的代码产生影响。

编写测试类main.cpp

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
#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】