C++单例模式详解

目录

[0. 前言](#0. 前言)

[1. 懒汉式单例模式](#1. 懒汉式单例模式)

[1.1 最简单的单例模式](#1.1 最简单的单例模式)

[1.2 防止内存泄漏](#1.2 防止内存泄漏)

[1.2.1 智能指针的方法](#1.2.1 智能指针的方法)

[1.2.2 静态嵌套的方法](#1.2.2 静态嵌套的方法)

[1.3 保证线程安全](#1.3 保证线程安全)

[1.4 C++11版本的优雅解决方案](#1.4 C++11版本的优雅解决方案)

[2. 饿汉式单例模式](#2. 饿汉式单例模式)


0. 前言

起因是在程序中重复声明了一个单例模式的变量,后来程序怎么调都不对,最后发现变量是用单例模式,修改是全局的,所以决定好好梳理一下单例模式。

首先,为什么要用单例模式,就是因为我们希望一个类只有唯一一个实例,并且提供一个全局的访问点。从这个描述不难看出,这个实例应该是要static来修饰的。实际情况中,比如我们想申请一个内存池,程序都用这一块内存池,那么就可以单例模式来实现

1. 懒汉式单例模式

1.1 最简单的单例模式

先来看看最简单的单例模式怎么写,然后分析一下有什么问题。

cpp 复制代码
#include <iostream>
using namespace std;

// 最简单的单例模式
class Singleton{
public:
    // 获取实例的接口
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton(); // 使用new来创建一个新的实例对象
        } else {
            cout << "重复创建,返回已创建的实例。" << endl;
        }
        return instance;
    }
private:
    // 静态私有对象
    static Singleton* instance;
    // 构造函数一定要私有,外部无法直接访问构造函数
    Singleton() {cout << "运行构造函数" << endl;};
    ~Singleton() {cout << "运行析构函数" << endl;};
};
// 要在类外进行初始化!!!
Singleton* Singleton::instance = nullptr;

int main(){
    Singleton* s1 = Singleton::getInstance();
    Singleton* s2 = Singleton::getInstance();
    
    return 0;
}

这里有两个点需要特别的注意:

  • 单例模式的类需要有一个静态私有的对象,是这个类的实例,且必须在类外进行初始化。
  • 获取实例的接口getInstance()函数式可以被访问和调用的,但是必须返回static类型的变量,实际上就是返回这个类的唯一实例
  • 补充下析构函数私有化的原因:保证只能在堆上new一个新的类对象。因为C++是一个静态绑定的语言。在编译过程中,所有的非虚函数调用都必须分析完成,即使是虚函数也需检查可访问性。当在栈上生成对象时,对象会自动析构,也就说析构函数必须可以访问。而堆上生成对象,由于析构时机由程序员控制,所以不一定需要析构函数。

按照上面的写法,基本上满足了单例模式的初衷,要一个只有一个实例的类。但是存在两个问题,一个是因为使用到了new进行创建,就需要人为进行delete的释放操作,否则就会造成内存泄漏 。第二个是程序乍一看只会创建一块内存空间,但是如果考虑多线程,那么就有可能多个线程分别创建了多块内存空间的实例,与我们设计单例模式的初衷相违背。

1.2 防止内存泄漏

1.2.1 智能指针的方法

运行1.1的程序,结果为:

运行构造函数
重复创建,返回已创建的实例。

可以发现,并没有调用析构函数。这里,补充一下析构函数的作用:释放对象的使用资源,并销毁对象的非static数据成员。而我们定义的instance成员变量static的,所以无法直接使用析构函数进行释放。 虽然事例的简单程序在运行完之后static变量会自动释放,但是在很多复杂的程序中,使用完instance却不释放是非常致命的会导致内存泄漏的问题。这里采用智能指针的方法,并借用智能指针的reset函数,定义一个销毁的成员函数,通过这个成员函数调用delete来释放我们创建的new内存,达到析构的目的。看看下面的实现。

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

class Singleton{
public:
    // 公有接口获取唯一实例
    static shared_ptr<Singleton> getInstance() {
        if (instance == nullptr) {
            instance.reset(new Singleton(), destoryInstance);
        }
        else {
            cout << "重复创建,返回异创建的实例。" << endl;
        }
        return instance;
    }
    // 定义销毁的实例
    static void destoryInstance(Singleton* x) {
        cout << "自定义释放实例" << endl;
        delete x;
    }
private:
    Singleton() {cout << "运行构造函数。" << endl;};
    ~Singleton() {cout << "运行析构函数。" << endl;};
private:
    // 静态私有对象
    static shared_ptr<Singleton> instance;
};

// 初始化
shared_ptr<Singleton> Singleton::instance;


int main(){
    shared_ptr<Singleton> s1 = Singleton::getInstance();
    shared_ptr<Singleton> s2 = Singleton::getInstance();
    return 0;
}

运行结果为:

cpp 复制代码
运行构造函数。
重复创建,返回异创建的实例。
自定义释放实例
运行析构函数。

可以看到,我们通过智能指针,在使用完instance资源后调用了自定义的释放函数,即delete了new出来的空间,达到了运行析构函数的目的,防止了内存泄漏。

1.2.2 静态嵌套的方法

解决内存的泄漏的方法,总之是要把释放的过程先写好,不能靠用户每次自己释放。对于本次分享的例子,就是要把delete放进代码里。除了利用智能指针的释放函数来调用delete之外,也可以显式的调用delete函数,要单独嵌套一个类,把这个delete函数放进嵌套类的公有析构函数中。实现过程如下:

cpp 复制代码
#include <iostream>
using namespace std;
class Singleton{
public:
    // 公有接口获取唯一实例
    static Singleton* getInstance() {
        if (instance == nullptr) {
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        else {
            cout << "重复创建,返回已创建的实例。" << endl;
        }
        return instance;
    }
private:
    Singleton() {cout << "运行构造函数。" << endl;};
    ~Singleton() {cout << "运行析构函数。" << endl;};
    // 定义一个删除器
    class Deleter {
    public:
        Deleter() {};
        ~Deleter() {
            if (instance != nullptr) {
                cout << "删除器启动。" << endl;
                delete instance;
                instance = nullptr;
            }
        }
    };
    static Deleter deleter; // 删除器也是静态成员变量
private:
    // 静态私有对象
    static Singleton* instance;
};

// 初始化
Singleton* Singleton::instance = nullptr;
Singleton::Deleter Singleton::deleter;


int main()
{
    Singleton* s1 = Singleton::getInstance();
    Singleton* s2 = Singleton::getInstance();

    return 0;
}

运行结果为:

cpp 复制代码
运行构造函数。
重复创建,返回已创建的实例。
删除器启动。
运行析构函数。

1.3 保证线程安全

首先修改一下1.1中的程序,主要是增加一些打印,然后用多个线程创建Singleton的实例,看看是否每个线程都是访问的同一个内存地址。

cpp 复制代码
#include <iostream>
#include <thread>
using namespace std;

class Singleton{
public:
    // 获取实例的接口
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton(); // 使用new来创建一个新的实例对象
            cout << "创建地址为:" << instance << endl;
        } else {
            cout << "重复创建,返回已创建的实例。" << endl;
        }
        return instance;
    }
private:
    // 静态私有对象
    static Singleton* instance;
    // 构造函数一定要私有,外部无法直接访问构造函数
    Singleton() {cout << "运行构造函数" << endl;};
    ~Singleton() {cout << "运行析构函数" << endl;};
};
// 要在类外进行初始化!!!
Singleton* Singleton::instance = nullptr;

int main(){
    // Singleton* s1 = Singleton::getInstance();
    // Singleton* s2 = Singleton::getInstance();
    thread t1([] {Singleton* s1 = Singleton::getInstance();});
    thread t2([] {Singleton* s2 = Singleton::getInstance();});
    t1.join();
    t2.join();
    return 0;
}

运行结果如下:

cpp 复制代码
运行构造函数
创建地址为:0x7f0988000b60
运行构造函数
创建地址为:0x7f0980000b60

可以发现,两个线程分别new出了一段内存空间(有一定几率是同一段,会报重复创建)。显然,这违背了我们单例模式的初衷。

解决方法是进行加锁,让一个线程先执行完,另一个线程才能获得new的权限。代码如下:

cpp 复制代码
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;

class Singleton{
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
        lock_guard<mutex> l(mutex1); // 加锁保证线程安全
        if (instance == nullptr) {
            instance = new Singleton();
            cout << "创建地址为:" << instance << endl;
            }
        }
        else {
            cout << "重复创建,返回已创建的实例。" << endl;
        }
        return instance;
    }
private:
    static mutex mutex1;// 锁
    static Singleton* instance;
    Singleton() {cout << "运行构造函数" << endl;};
    ~Singleton() {cout << "运行析构函数" << endl;};
};

// 初始化
Singleton* Singleton::instance = nullptr;
mutex Singleton::mutex1;

int main(){
    thread t1([](){Singleton* s1 = Singleton::getInstance();});
    thread t2([](){Singleton* s2 = Singleton::getInstance();});
    t1.join();
    t2.join();

    return 0;
}

运行结果为:

cpp 复制代码
运行构造函数
创建地址为:0x7f90d4000b60
重复创建,返回已创建的实例。

加锁后,即使是多个线程,也只会申请一块内存空间。

1.4 C++11版本的优雅解决方案

上面只是为了将这个问题表述清楚,在C++11中,static变量是可以保证线程安全的,同时直接用static变量而不用new,就可以获得线程安全的且无内存泄漏的优雅写法,如下:

cpp 复制代码
#include <iostream>
#include <thread>
using namespace std;
class Singleton{
public:
    // 公有接口获取唯一实例
    static Singleton* getInstance() {
        static Singleton instance;
        cout << "地址为:" << &instance << endl;
        return &instance;
    }

private:
    Singleton() {cout << "运行构造函数" << endl;};
    ~Singleton() {cout << "运行析构函数" << endl;};
};

int main()
{
    thread t1([] {Singleton* s1 = Singleton::getInstance();});
    thread t2([] {Singleton* s2 = Singleton::getInstance();});
    t1.join();
    t2.join();
    return 0;
}

运行结果如下:

cpp 复制代码
运行构造函数
地址为:0x55b7df269152
地址为:0x55b7df269152
运行析构函数

可以看到访问的内存地址一样,析构函数也正常的运行了。

2. 饿汉式单例模式

饿汉式和懒汉式的差别是,饿汉式提前进行了创建,而如果提前创建static变量,那么在程序开始前这个变量就创建好了,因此不存在线程不安全的问题,只需要保证不内存泄漏即可。用智能指针的方式实现代码如下:

cpp 复制代码
#include <iostream>
#include <thread>
using namespace std;
class Singleton{
public:
    // 公有接口获取唯一实例
    static shared_ptr<Singleton> getInstance() {
        cout << "地址为:" << instance << endl;
        return instance;
    }
    // 定义销毁的实例
    static void destoryInstance(Singleton* x) {
        cout << "自定义释放实例" << endl;
        delete x;
    }

private:
    Singleton() {cout << "运行构造函数" << endl;};
    ~Singleton() {cout << "运行析构函数" << endl;};
private:
    // 静态私有对象
    static shared_ptr<Singleton> instance;
};

// 初始化
shared_ptr<Singleton> Singleton::instance(new Singleton(), destoryInstance);

int main(){
    thread t1([] {shared_ptr<Singleton> s1 = Singleton::getInstance();});
    thread t2([] {shared_ptr<Singleton> s2 = Singleton::getInstance();});
    t1.join();
    t2.join();
    return 0;
}

运行结果为:

cpp 复制代码
运行构造函数。
地址为:0x55977895ceb0
地址为:0x55977895ceb0
自定义释放实例
运行析构函数。

也可以考虑优雅的写法:

cpp 复制代码
#include <iostream>
#include <thread>
using namespace std;
class Singleton{
public:
    // 公有接口获取唯一实例
    static Singleton* getInstance() {
        static Singleton instance;
        cout << "地址为:" << &instance << endl;
        return &instance;
    }

private:
    // 私有构造函数
    Singleton() {cout << "运行构造函数。" << endl;};
    // 私有析构函数
    ~Singleton() {cout << "运行析构函数。" << endl;};
};


int main(){
    thread t1([] {Singleton* s1 = Singleton::getInstance();});
    thread t2([] {Singleton* s2 = Singleton::getInstance();});
    t1.join();
    t2.join();
    return 0;
}

输出的结果为:

cpp 复制代码
运行构造函数。
地址为:0x55aa7a895152
地址为:0x55aa7a895152
运行析构函数。
相关推荐
_WndProc14 分钟前
C++ 日志输出
开发语言·c++·算法
薄荷故人_15 分钟前
从零开始的C++之旅——红黑树及其实现
数据结构·c++
m0_7482400216 分钟前
Chromium 中chrome.webRequest扩展接口定义c++
网络·c++·chrome
qq_4335545423 分钟前
C++ 面向对象编程:+号运算符重载,左移运算符重载
开发语言·c++
努力学习编程的伍大侠27 分钟前
基础排序算法
数据结构·c++·算法
yuyanjingtao1 小时前
CCF-GESP 等级考试 2023年9月认证C++四级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
闻缺陷则喜何志丹1 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
charlie1145141912 小时前
C++ STL CookBook
开发语言·c++·stl·c++20
小林熬夜学编程2 小时前
【Linux网络编程】第十四弹---构建功能丰富的HTTP服务器:从状态码处理到服务函数扩展
linux·运维·服务器·c语言·网络·c++·http
倔强的石头1062 小时前
【C++指南】类和对象(九):内部类
开发语言·c++