c++ 设计模式之单例详解

 文章目录


一、单例的概念及优缺点

1 定义:

要求一个类只能生成一个对象,所有对象对它的依赖相同。

单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;

2 定义单例的步骤:

定义一个单例类:

  1. 私有化它的构造函数,以防止外界创建单例类的对象;
  2. 使用类的私有静态指针变量指向类的唯一实例;
  3. 使用一个公有的静态方法获取该实例。

注意:当无法通过实例化对象访问类内的成员变量和函数,通过类名去访问类的属性和方法应该使用静态的对象去访问。且类内的静态成员变量在使用时应该在类外进行初始化。

私有化它的构造函数可以使用7-9的方式,或者15-19的方式。

 3 优点

  • 只有一个实例,减少内存开支。应用在一个经常被访问的对象上。
  • 减少系统的性能开销,应用启动时,直接产生一单例对象,用永久驻留内存的方式。
  • 避免对资源的多重占用。
  • 可在系统设置全局的访问点,优化和共享资源访问。

4 缺点

  • 一般没有接口,扩展困难。原因:接口对单例模式没有任何意义;要求“自行实例化”,并提供单一实例,接口或抽象类不可能被实例化。(当然,单例模式可以实现接口、被继承,但需要根据系统开发环境判断。
  • 单例模式对测试是不利的。如果单例模式没完成,是不能进行测试的。
  • 单例模式与单一职责原则有冲突。原因:一个类应该只实现一个逻辑,而不关心它是否是单例,是不是要单例取决于环境;单例模式把“要单例”和业务逻辑融合在一个类。

5 使用场景

  • 要求生成唯一序列化的环境.
  • 项目需要的一个共享访问点或共享的数据点.
  • 创建一个对象需要消耗资源过多的情况。如:要访问IO和 数据库等资源。
  • 需要定义大量的静态常量和静态方法(如工具类)的环境。可以采用单例模式或者直接声明static的方式。

6 注意事项

  • 类中其他方法,尽量是static。

7 基础要点

  • 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数(默认构造,拷贝构造,拷贝赋值运算符)设为 private或protected)
  • 线程安全
  • 禁止赋值和拷贝
  • 用户通过接口获取实例:使用 static 类成员函数

二、实现单例的几种方式

懒汉式单例

1、原始懒汉式单例模式

        懒汉式单例就是需要使用这个单例对象的时候才去创建这个单例对象。

        {懒汉式(Lazy-Initialization)的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象。好处是如果被调用就不会占用内存。}

#include <iostream>
#include <stdexcept>
#include <thread>
#include <mutex>

#ifdef WIN32
#include <windows.h>
#define SLEEP(x) Sleep(x)
#else
#include <unistd.h>
#define SLEEP(x) usleep(x*1000)
#endif

using namespace std;

//懒汉式单例
class Singleton {
private:
    static Singleton *singleton;//定义静态成员变量
    Singleton() = default;//default 表示使用构造函数的默认行为,只是将其权限设置为私有
    Singleton(const Singleton& s) = default; //禁止使用拷贝构造函数 实例的s可写可不写(const Singleton&)
    Singleton& operator=(const Singleton& s) = default; //禁止使用拷贝赋值运算符
public:
  //静态成员变量需要静态成员函数进行访问
    static Singleton* getInstance(){
        if (Singleton::singleton == nullptr){
            SLEEP(10);//休眠,模拟创建实例的时间
            singleton = new Singleton(); //创建单例实例对象
        }
        return singleton;
    }
};

// 静态成员变量在使用时必须在类外初始化
Singleton* Singleton::singleton = nullptr;
// 定义一个互斥锁
mutex m;

void print_address(){
    // 获取实例
    Singleton* singleton1 = Singleton::getInstance();
    // 打印singleton1地址
    m.lock(); // 锁住,保证只有一个线程在打印地址
    cout<<singleton1<<endl;
    m.unlock();// 解锁
}

int main(){
    thread threads[10];

    // 创建10个线程
    for (auto&t : threads)
        t = thread(print_address);
    // 对每个线程调用join,主线程等待子线程完成运行
    for (auto&t : threads)
        t.join();
}

运行结果:

0x7ff280000b20
0x7ff298000b20
0x7ff290000b20
0x7ff288000b20
0x7ff278000b20
0x7ff260000b20
0x7ff268000b20
0x7ff264000b20
0x7ff258000b20
0x7ff270000b20

        可以看出,结果里面有好几个不同地址的实例! 所以,这种单例模式不是线程安全的。原因是,当几个线程同时执行到语句if (Singleton::singleton == nullptr)时,singleton都还没有被创建,所以就重复创建了几个实例。

2、线程安全的单例模式

        为了编写线程安全的单例模式,可以锁住getInstance函数,保证同时只有一个线程访问getInstance函数(为了节省篇幅,相同的部分不在代码中再次给出)。

using namespace std;

//线程安全的懒汉式单例
mutex m1;
class Singleton {
private:
    static Singleton *singleton;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
public:
    static Singleton* getInstance() {
        m1.lock(); // 加锁,保证只有一个线程在访问下面的语句
        if (Singleton::singleton == nullptr){
            SLEEP(10); //休眠,模拟创建实例的时间
            singleton = new Singleton();
        }
        m1.unlock();//解锁
        return singleton;
    }
};

运行输出:

0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20

        可以发现,所有线程获取到的实例的地址都相同。整个程序中只有一个Singleton实例。原因是进入getInstance函数之后,立马锁住创建实例的语句,保证只有一个线程在访问创建实例的代码。

3、锁住初始化实例语句的方式

        仅仅对创建实例的语句进行加锁,是否是线程安全的呢?

//线程安全的懒汉式单例
mutex m1;
class Singleton {
private:
    static Singleton *singleton;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
public:
    static Singleton* getInstance() {
        if (Singleton::singleton == nullptr){
            SLEEP(100); //休眠,模拟创建实例的时间
            m1.lock();  // 加锁,保证只有一个线程在创建实例
            singleton = new Singleton();
            m1.unlock();//解锁
        }
        return singleton;
    }
};

运行结果:

0x7ff7f0000b20
0x7ff7e8000b20
0x7ff7f0000f50
0x7ff7ec000b20
0x7ff7e0000b20
0x7ff7ec000b40
0x7ff7f0000f70
0x7ff7e0000b40
0x7ff7f0000f90
0x7ff7ec000b60

         这种方式不是线程安全的。 因为当线程同时执行到语句if (Singleton::singleton == nullptr)时,singleton都还没有被创建,故会条件为真,多个线程都会创建实例,尽管不是同时创建。

4、锁住初始化实例语句之后再次检查实例是否被创建

//线程安全的懒汉式单例
mutex m1;
class Singleton {
private:
    static Singleton *singleton;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
public:
    static Singleton* getInstance() {
        if (Singleton::singleton == nullptr){
            SLEEP(100); //休眠,模拟创建实例的时间
            m1.lock();  // 加锁,保证只有一个线程在访问线程内的代码
            if (Singleton::singleton == nullptr) { //再次检查
                singleton = new Singleton();
            }
            m1.unlock();//解锁
        }
        return singleton;
    }
};

运行输出:

0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20

        可以看出,这种方式是线程安全的。并且没有第2种代码简洁。 


饿汉式单例

        先实例化该单例类,而不是像之前一样初始化为空指针。

using namespace std;

//线程安全的饿汉式单例
class Singleton {
private:
    static Singleton *singleton;
    Singleton() try{
        // 构造本单利模式的代码
    }catch (exception& e){
        cout << e.what() << endl;
        // 在这里处理可能的异常情况
        throw;
    }

    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
public:
    static Singleton* getInstance() {
        return singleton;
    }
};

// 必须在类外初始化
Singleton* Singleton::singleton = new Singleton();

// 定义一个互斥锁
mutex m;

运行输出:

0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70

         可以看出singleton的实例确实只有一个。饿汉式单例会在程序开始之前就被创建,所以是线程安全的。由于创建的单例是在全局变量区,所以需要处理构造函数中可能出现的异常:

Singleton() try{
        // 构造本单利模式的代码
    }catch (exception& e){
        cout << e.what() << endl;
        // 在这里处理可能的异常情况
        throw;
    }

 这里涉及到的知识是:很少有人知道的c++中的try块函数


三、最推荐的懒汉式单例--静态局部变量

1.写法一

#include <iostream>
 
class Singleton
{
public:
    ~Singleton(){
        std::cout<<"destructor called!"<<std::endl;
    }
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;

    static Singleton& get_instance(){
        static Singleton instance;//定义一个静态局部对象,使用无参的构造函数创建,此处instance对象前加static,即使出了这个大括号,其对应的是全局数据区的一个内存,在程序运行期间其一直存在,不被析构。也即每一次调用get_instance()返回的instance都是同一块内存区上的数据
        return instance;
 
    }
private:
    Singleton(){
        std::cout<<"constructor called!"<<std::endl;
    }
};
 
int main(int argc, char *argv[])
{
    Singleton& instance_1 = Singleton::get_instance();
    Singleton& instance_2 = Singleton::get_instance();
    return 0;
}
 

运行结果:

constructor called!
destructor called!

         这种方法又叫做 Meyers' SingletonMeyer's的单例, 是著名的写出《Effective C++》系列书籍的作者 Meyers 提出的。所用到的特性是在C++11标准中的Magic Static特性:

        如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。也即等待 static Singleton instance先完成一次初始化

更详细的说法:

local static 对象(函数内)

        对于local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句。

        在C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。所以,local static对象在多线程环境下的重复构造问题是需要解决的。

        而C++11则在语言规范中解决了这个问题。C++11规定,在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成。

这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。

C++静态变量的生存期 是从声明到程序结束,这也是一种懒汉式。

这是最推荐的一种单例实现方式:

  1. 通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
  2. 不需要使用共享指针,代码简洁;
  3. 注意在使用的时候需要声明单例的引用 Single& 才能获取对象。

最推荐的懒汉式单例--静态局部变量

2.写法二

#include <iostream>
using namespace std;
//懒汉模式 -> 什么时后使用这个单例对象,什么时候再去创建对应的实例。
//使用静态的局部对象解决线程安全问题 ->编译器必须支持C++11
//适用于多线程下的访问且使用的懒汉模式!!!工程多应用此类单例设计模式//
//静态数据在全局数据区默认值是0,在全局数据区只要它是0,默认没有被初始化

class TaskQueue {
public:                                   //此处public可换成protected
  TaskQueue(const TaskQueue &t) = delete; //禁用拷贝构造函数
  TaskQueue &operator=(const TaskQueue &t) = delete; //禁止使用拷贝赋值运算符

  static TaskQueue *getinstance() {
    static TaskQueue task; //定义一个静态局部对象,使用无参的构造函数创建,此处instance对象前加static,即使出了这个大括号,其对应的是全局数据区的一个内存,在程序运行期间其一直存在,不被析构。也即每一次调用get_instance()静态方法返回的instance都是同一块内存区上的数据

    return &task; //返回的是地址,必须是静态的,要不出了大括号就被析构了
  }

  void print() { cout << "我是单例对象的一个成员函数 " << endl; }

private:
  TaskQueue() = default;
  //此处可以使用8-9禁止赋值和拷贝或者15-16两种方式
  //    TaskQueue(const TaskQueue& t) = default; //禁用拷贝构造函数
  //    TaskQueue& operator = (const TaskQueue& t) = default;
  //    //禁止使用拷贝赋值运算符
};

int main() {
    TaskQueue *instance = TaskQueue::getinstance();  //获取唯一的实例对象,被instance指针保存下来
    instance->print();
    return 0;
}

运行结果:

我是单例对象的一个成员函数 

最推荐的懒汉式单例--静态局部变量

3.写法三

如果每一个类都要设计一次单例,会使得代码冗余,所以可以使用类模板来设计一个通用的单例

template <typename T>

静态成员需要类外初始化 

调用不同类模板的单例(以A和B类为例)


总结

        本文为对设计模式单例的学习总结,主要参考下述博主,如有侵权,可联系删除。如有表述错误之处,恳请指证,谢谢!

参考博客:

12. 如何创建一个单例模式的类_哔哩哔哩_bilibili

C++ 单例模式:从设计到实现_哔哩哔哩_bilibili

C++单例模式详解_c++ 单例模式-CSDN博客

C++单例模式_c++ 单例-CSDN博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值