C++设计模式-单例(1)

设计模式:在解决某一类问题场景时,有既定的,优秀的代码框架可以直接使用,其优势如下:

  • 代码更易于维护,代码的可读性,复用性,可移植性,健壮性会更好
  • 当软件原有需求有变更或者增加新的需求时,合理的设计模式的应用,能够做到软件设计要求的开-闭原则,即对修改关闭,对扩展开放,使软件原有功能修改,新功能扩充非常灵活
  • 合理的设计模式的选择,会使软件设计更加模块化,积极的做到软件设计遵循的根本原则高内聚,低耦合

软件设计开-闭原则

开闭原则是软件设计中的一个重要原则,指的是软件实体(如类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在不修改现有代码的基础上,通过扩展新功能来满足新的需求,从而提高软件的灵活性和可维护性

对扩展开放:允许在现有系统中添加新功能

对修改关闭:不需要修改现有的代码来添加新功能,避免引入新错误

通过遵循开闭原则,可以提高代码的可复用性和稳定性。常用的方法包括使用抽象类、接口以及多态等技术

创建型设计模式

创建型设计模式关注对象的创建过程,旨在抽象实例化的过程。以下是几种常见的创建型设计模式

工厂方法模式(Factory Method Pattern)

抽象工厂模式(Abstract Factory Pattern)

单例模式(Singleton Pattern)

生成器模式(Builder Pattern)

原型模式(Prototype Pattern)



一、单例模式

1.相关概念

单例模式(Singleton Pattern)是一种创建型设计模式,旨在确保一个类只有一个实例,并提供一个全局访问点

特点

唯一性:一个类只能有一个实例

全局访问点:提供一个全局的访问点,可以访问到该实例


2.应用场景

单例模式适用于以下场景:

需要控制资源的访问:如数据库连接池、日志记录器等

需要全局唯一实例的类:如配置类、缓存类等

需要延迟实例化的类:如需要在程序运行期间根据需要创建实例,而不是在程序启动时创建


3.单例模式实现方法

常见的方法有一下几种

饿汉式

  • 在类加载时就创建实例
  • 简单,但在类加载时就初始化实例,可能造成资源浪费

懒汉式

  • 在第一次使用时才创建实例
  • 需要考虑线程安全问题

双重检查锁

  • 结合懒汉式和同步块,确保线程安全并减少同步开销

静态内部类

  • 利用类加载机制,保证线程安全和延迟加载


二、实现

1.饿汉式

饿汉式的单例模式,在还未获取类实例对象之前,类实例对象就已经产生了,需要注意如下几点:

限制构造函数的访问方式

  • 构造函数私有化

定义一个唯一的类实例对象

  • 私有化,通过static修饰

定义一个静态接口函数,获取唯一的类实例对象

  • 接口函数return唯一实例对象地址

复制构造函数/赋值运算符均禁止(显示禁止)

代码实例

Singleton.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once
#include <iostream>

class Singleton
{
public:
// 定义一个静态的接口函数,获取唯一的类实例对象
static Singleton *getInstance()
{
return &instance;
}
private:
// 构造函数的私有化
Singleton(){}
// 定义一个私有的唯一的类的实例对象
static Singleton instance;
// 复制构造函数禁止
Singleton(const Singleton&) = delete;
// 赋值运算符禁止
Singleton& operator=(const Singleton&) = delete;
};

Singleton.cpp

类的静态变量初始化需要放到源文件(CPP)中,如果在头文件中初始化静态变量,当该头文件被多个源文件包含时,会导致重复定义的问题。而在源文件中初始化则可以避免这个问题

1
2
3
#include "Singleton.h"

Singleton Singleton::instance;

测试代码

通过接口函数,获取三个实例对象,将其实例对象的地址进行输出,可知地址均相同,表示单例模式下,只能得到该类的一个实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "Singleton/Singleton.h"
using namespace std;

int main()
{
Singleton *p1 = Singleton::getInstance();
Singleton *p2 = Singleton::getInstance();
Singleton *p3 = Singleton::getInstance();

cout << (void *)p1 << endl;
cout << (void *)p2 << endl;
cout << (void *)p3 << endl;
}

运行结果:

1
2
3
4
/home/zxz/Proj/C_C++/DesignPatterns/cmake-build-debug/DesignPatterns
0x555a19038152
0x555a19038152
0x555a19038152

重要知识点

  • static修饰的变量在进程内存静态数据区(数据段)数据段中的变量(包括全局变量、静态变量和常量)在程序的main函数执行之前初始化。这是因为这些变量在程序加载到内存时就已经被分配和初始化,以确保它们在main函数以及其他任何代码执行之前就已经处于准备好的状态
  • 饿汉式单例模式,是线程安全的。在类加载的时候,静态成员 instance 就会被初始化并创建实例。这保证了实例在多线程环境中是安全的,因为类加载机制是由编译器和JVM保证的

优缺点:

优点:

  • 线程安全:饿汉式单例在类加载时创建实例,类加载过程是线程安全的。
  • 实现简单:不需要额外的同步机制,代码实现简单

缺点:

  • 资源浪费:即使没有使用单例实例,实例也会在类加载时创建。如果实例的创建依赖于外部资源或耗时操作,会导致程序启动变慢
  • 不灵活:在一些场景下,可能需要在第一次使用时才创建实例(懒加载),饿汉式单例不适合这种情况

2.懒汉式

类唯一的实例对象直到第一次获取的时候才产生

  • 在饿汉式的基础上,将私有的唯一的类的实例对象定义为指针对象,并且在类外进行初始化为nullptr
1
2
3
4
5
6
7
8
// Singleton.h
class Singleton{
private:
static Singleton* instance;
};

// Singleton.cpp
Singleton* Singleton::instance = nullptr;
  • 并且静态函数接口获取唯一的实例对象时,先对实例对象进行判断,若为nullptr,则动态创建一个类实例对象;否则返回该类唯一的实例对象地址 (第一点非重点,第二点才是重点
1
2
3
4
5
6
7
8
9
// 定义一个静态的接口函数,获取唯一的类实例对象
static Singleton *getInstance()
{
if(instance == nullptr)
{
instance = new Singleton();
}
return instance;
}

代码实例

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
class Singleton
{
public:
// 定义一个静态的接口函数,获取唯一的类实例对象
static Singleton *getInstance()
{
if(instance == nullptr)
{
instance = new Singleton();
}
return instance;
}

private:
// 构造函数的私有化
Singleton(){}
// 定义一个私有的唯一的类的实例对象指针
static Singleton *instance;
// 复制构造函数禁止
Singleton(const Singleton&) = delete;
// 赋值运算符禁止
Singleton& operator=(const Singleton&) = delete;
};

Singleton.cpp

1
Singleton *Singleton::instance = nullptr;

重要知识点

  • 懒汉模式的单例模式,不是线程安全的,因为懒汉式的静态接口函数非可重入函数
  • 可重入函数:在多线程的条件下,可被多个线程调用并且不会产生竞态条件,可以被多个线程重复调用,而不会发生线程安全问题
    • 假设,线程1,第一次进入getInstance函数,并且构造对象,但是还并未给instance进行赋值,此时instance==nullptr还是成立;若此时CPU时间片被线程2抢占,进入getInstance函数,构造对象,并且给instance进行赋值,返回instance指针变量中的地址。线程1,继续抢占CPU时间片,并且给instance进行赋值,最后返回。这时由于getInstance函数的不可重入性,出现了线程安全问题,导致为该类创建了两个实例对象

3.线程安全的懒汉式

在懒汉模式的基础上,对临界区代码段,创建互斥锁,保证原子操作

(1)基础实现

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
class Singleton
{
public:
// 定义一个静态的接口函数,获取唯一的类实例对象
static Singleton *getInstance()
{
// 加锁
pthread_mutex_lock(&mtx);
if(instance == nullptr)
{
instance = new Singleton();
}
// 解锁
pthread_mutex_unlock(&mtx);
return instance;
}

private:
// 构造函数的私有化
Singleton(){}
// 定义一个私有的唯一的类的实例对象指针
static Singleton *instance;
// 复制构造函数禁止
Singleton(const Singleton&) = delete;
// 赋值运算符禁止
Singleton& operator=(const Singleton&) = delete;

// 互斥锁 (可以直接使用 static std::mutex mtx静态初始化的方式,源文件就可以省略初始化)
static pthread_mutex_t mtx;
};

Singleton.cpp

1
2
Singleton *Singleton::instance = nullptr;
pthread_mutex_t Singleton::mtx = PTHREAD_MUTEX_INITIALIZER;

重要知识点

但是这样粗暴的加锁方式,会导致在单线程的环境下,也会频繁加锁,加锁的力度过大

(2)双重检查锁实现(重点)

保证在单线程的环境下,每次调用getInstance函数不需要重复进行加锁

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
32
33
34
35
36
class Singleton
{
public:
// 定义一个静态的接口函数,获取唯一的类实例对象
static Singleton *getInstance()
{


if(instance == nullptr) // 第一次检查
{
// 加锁--双重检查
pthread_mutex_lock(&mtx);
if(instance == nullptr) // 第二次检查
{
instance = new Singleton(); // 确保实例仅创建一次
}
// 解锁
pthread_mutex_unlock(&mtx);
}

return instance;
}

private:
// 构造函数的私有化
Singleton(){}
// 定义一个私有的唯一的类的实例对象指针
static Singleton *instance;
// 复制构造函数禁止
Singleton(const Singleton&) = delete;
// 赋值运算符禁止
Singleton& operator=(const Singleton&) = delete;

// 互斥锁 (可以直接使用 static std::mutex mtx静态初始化的方式,源文件就可以省略初始化)
static pthread_mutex_t mtx;
};

Singleton.cpp

1
2
Singleton *Singleton::instance = nullptr;
pthread_mutex_t Singleton::mtx = PTHREAD_MUTEX_INITIALIZER;
(3)静态内部类实现

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
class Singleton
{
public:
// 定义一个静态的接口函数,获取唯一的类实例对象引用
static Singleton &getInstance()
{
return SingletonHolder::instance; // 静态内部类,延迟加载,线程安全
}

private:
// 构造函数的私有化
Singleton(){}
// 复制构造函数禁止
Singleton(const Singleton&) = delete;
// 赋值运算符禁止
Singleton& operator=(const Singleton&) = delete;

// 静态内部类中定义静态类实例
class SingletonHolder
{
public:
static Singleton instance;
};
};

Singleton.cpp

1
Singleton Singleton::SingletonHolder::instance;

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "Singleton/Singleton.h"
using namespace std;

int main()
{
Singleton &p1 = Singleton::getInstance();
Singleton &p2 = Singleton::getInstance();
Singleton &p3 = Singleton::getInstance();

cout << (void *)&p1 << endl;
cout << (void *)&p2 << endl;
cout << (void *)&p3 << endl;
}
(4)局部静态变量实现(重点)

《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作

Singleton.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Singleton
{
public:
// 定义一个静态的接口函数,获取唯一的类实例对象
static Singleton *getInstance()
{
// 函数静态局部变量的初始化,在汇编指令上已经自动添加线程互斥指令
// 因此如下的写法是线程俺去的
static Singleton instance; // 定义以及初始化一个唯一的类实例对象
return &instance;
}

private:
// 构造函数的私有化
Singleton(){}
// 复制构造函数禁止
Singleton(const Singleton&) = delete;
// 赋值运算符禁止
Singleton& operator=(const Singleton&) = delete;
};

Singleton.cpp

1
// 不需要做什么

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "Singleton/Singleton.h"
using namespace std;

int main()
{
Singleton *p1 = Singleton::getInstance();
Singleton *p2 = Singleton::getInstance();
Singleton *p3 = Singleton::getInstance();

cout << (void *)p1 << endl;
cout << (void *)p2 << endl;
cout << (void *)p3 << endl;
}