C++设计模式——单例模式(Singleton Pattern)

C++设计模式——单例模式(Singleton Pattern)

微信公众号:幼儿园的学霸

目录

定义

单例模式顾名思义,保证一个类仅可以有一个实例化对象,并且提供一个可以访问它的全局接口。实现单例模式必须注意一下几点:

  • 单例类只能由一个实例化对象。
  • 单例类必须自己提供一个实例化对象。
  • 单例类必须提供一个可以访问唯一实例化对象的接口。

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

唯一:唯一的实例;任何位置获取的都是那个唯一实例

代码示例

单例模式分为懒汉和饿汉单例模式

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

懒汉模式

懒汉:故名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化一个对象。在访问量较小,甚至可能不会去访问的情况下,采用懒汉实现,这是以时间换空间。

线程/内存不安全方式

代码中采用了c++11的delete.在C++11之前,当我们希望一个类不能被拷贝,就会把构造函数定义为private,但是在C++11里就不需要这样做了,只需要在构造函数后面加上=delete来修饰下就可以了。

#include <bits/stdc++.h>

//
//有缺陷的懒汉单例模式
//缺陷:线程不安全、内存不安全(内存泄露)

class Singleton {
private:
    //构造函数私有
    Singleton() {
        std::cout << "Constructor called!" << std::endl;
    }

    Singleton(Singleton &) = delete;//明确拒绝
    Singleton &operator=(const Singleton &) = delete;

    static Singleton *m_instance_ptr;
public:
    ~Singleton() {
        std::cout << "Destructor called!" << std::endl;
    }

    static Singleton *get_instance() {
        if (m_instance_ptr == nullptr) {
            m_instance_ptr = new Singleton;
        }
        return m_instance_ptr;
    }

    void use() const { std::cout << "in use" << std::endl; }
};

//类的静态成员变量需要在类外进行初始化
Singleton *Singleton::m_instance_ptr = nullptr;

int main() {
    Singleton *instance = Singleton::get_instance();
    Singleton *instance_2 = Singleton::get_instance();
    instance->use();
    return 0;

    //运行结果:
    //Constructor called!
    //in use
}

从运行结果可以看到,获取了两次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这是个最基础版本的单例实现,但是它存在一些问题:

  • 线程安全的问题,当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_instance_ptr是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来; 解决办法:加锁
  • 内存泄漏. 注意到类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。解决办法: 使用共享指针;

因此,接下来提供一个改进的,线程安全的、使用智能指针的实现

智能指针+双检锁模式和智能指针+call_once模式

#include <bits/stdc++.h>

//
//线程安全/内存安全的懒汉单例模式
//智能指针+双检锁 or 智能指针+call_once

采用智能指针,双检锁
//class Singleton{
//public:
//    typedef std::shared_ptr<Singleton> Ptr;
//
//    //析构函数定义为public
//    ~Singleton(){
//        std::cout<<"Destructor called!"<<std::endl;
//    }
//    Singleton(Singleton&) =delete;
//    Singleton& operator=(const Singleton&) =delete;
//    static Ptr get_instance(){
//
//        // DCL "double checked lock"
//        if(m_instance_ptr==nullptr){//第一次检查
//            std::lock_guard<std::mutex> lk(m_mutex);
//            if(m_instance_ptr == nullptr){//第二次检查
//                m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
//                //由于构造函数是private,因此下面这种写法行不通
//                //m_instance_ptr = std::make_shared<Singleton>();
//            }
//        }
//        return m_instance_ptr;
//    }
//
//    void use() const { std::cout << "in use" << std::endl; }
//
//
//private:
//    Singleton(){
//        std::cout<<"Constructor called!"<<std::endl;
//    }
//    static Ptr m_instance_ptr;
//    static std::mutex m_mutex;//由于需要在static函数中调用,因此定义为static
//};
//
 静态成员变量初始化
//Singleton::Ptr Singleton::m_instance_ptr = nullptr;
//std::mutex Singleton::m_mutex;



//===============================//
//采用智能指针+call_once
//linux平台,CMakelists.txt需要
//target_link_libraries(NEAME,-pthread)才能使用call_once
class Singleton {
private:
    Singleton() {
        std::cout << "Constructor called!" << std::endl;
    }

    static void createInstance() {
        m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
    }

public:
    typedef std::shared_ptr<Singleton> Ptr;

    ~Singleton() {
        std::cout << "Destructor called!" << std::endl;
    }

    static std::shared_ptr<Singleton> get_instance() {
        static std::once_flag s_flag;
        //std::call_once(s_flag, createInstance);
        std::call_once(s_flag, [] { m_instance_ptr = std::shared_ptr<Singleton>(new Singleton); });
        return m_instance_ptr;
    }

    void use() const { std::cout << "in use" << std::endl; }

private:
    static std::shared_ptr<Singleton> m_instance_ptr;

};

//静态成员变量初始化
Singleton::Ptr Singleton::m_instance_ptr = nullptr;

int main() {
    Singleton::Ptr instance = Singleton::get_instance();
    Singleton::Ptr instance2 = Singleton::get_instance();
    instance->use();
    return 0;

    //两种情况下的运行结果:
    //Constructor called!
    //in use
    //Destructor called!
}

从运行结果可以看到,确实只构造了一次实例,并且发生了析构。

shared_ptrmutex都是C++11的标准,以上这种方法的优点是:

  • 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
  • 加了锁,使用互斥量来达到线程安全。这里使用了两个if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。

不足之处在于: 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。

还有更加严重的问题,在某些平台(与编译器和指令集架构有关),双检锁会失效!
观察上面智能指针+双检锁的代码,表面上这段代码是没有问题的,但实际还是存在问题,来源于CPU的乱序执行,c++new分为两步:

1.分配内存
2.调用构造函数

所以 pInst = new T();这句代码分为三步:

1.分配内存
2.在内存的位置上调用构造函数
3.将内存的地址复制给pInst

在这三步中,2和3的顺序是可以颠倒的。也就是说,完全有可能出现pInst的值已经不是nullptr,但对象仍然没有构造完毕,这时候如果出现另外一个对GetInstance的并发调用,此时第一个if内的pInst已经不为nullptr。所以这个调用会直接返回尚未构造完全的对象的地址以提供给用户使用。那么这个程序就存在崩溃的可能性。

上面代码中的第2中方法:智能指针+call_once的模式理论上可以避免上面的问题,查找资料,还没有查找到相关的介绍,因此可以理解为能够避免双检锁失效的问题。

关于上面介绍,可以搜索: C++11修复了双重检查锁定问题

不过,在使用单例模式的时候尽量用最推荐的模式 – 局部静态变量,也就是下面的代码。

局部静态变量模式

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

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。

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

#include <bits/stdc++.h>

//
//采用局部静态变量
//

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;
        return instance;

    }
    void use() const { std::cout << "in use" << std::endl; }

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();
    instance_1.use();
    return 0;

    //运行结果
    //Constructor called!
    //in use
    //Destructor called!
}

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

另外网上有人的实现返回指针而不是返回引用,

static Singleton* get_instance(){
    static Singleton instance;
    return &instance;
}

这样做并不好,理由主要是无法避免用户使用delete instance导致对象被提前销毁。还是建议大家使用返回引用的方式。

饿汉模式

饿汉:饿了肯定要饥不择食。所以在单例类定义的时候就进行实例化。在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。

  • 采用普通指针
#include <bits/stdc++.h>

//
//饿汉单例模式
//

//饿汉式:线程安全,注意一定要在合适的地方去delete它
//.h头文件
class Singleton
{
public:
    static Singleton* getInstance();
    void use() const { std::cout << "in use" << std::endl; }
    ~Singleton() {
        std::cout << "Destructor called!" << std::endl;
    }
private:
    Singleton(){//构造函数私有
        std::cout << "Constructor called!" << std::endl;}
    Singleton(const Singleton&) = delete;            //明确拒绝
    Singleton& operator=(const Singleton&) = delete; //明确拒绝
    static Singleton* m_pSingleton;
};
//.cpp文件
Singleton* Singleton::m_pSingleton = new Singleton();
Singleton* Singleton::getInstance()
{
    return m_pSingleton;
}



int main()
{
    Singleton* instance = Singleton::getInstance();
    instance->use();
    delete instance;//需要手动delete
    instance = nullptr;
    return 0;
    //运行结果
    //Constructor called!
    //in use
    //Destructor called!

    
}
  • 采用智能指针
#include <bits/stdc++.h>

//
//饿汉单例模式
//

//饿汉式:线程安全,同时采用智能指针的方式
//.h头文件
class Singleton {
public:
    typedef std::shared_ptr<Singleton> Ptr;

    static Ptr getInstance();

    void use() const { std::cout << "in use" << std::endl; }

    ~Singleton() {
        std::cout << "Destructor called!" << std::endl;
    }

private:
    Singleton() {//构造函数私有
        std::cout << "Constructor called!" << std::endl;
    }

    Singleton(const Singleton &) = delete;            //明确拒绝
    Singleton &operator=(const Singleton &) = delete; //明确拒绝
    static Ptr m_pSingleton;
};

//.cpp文件
Singleton::Ptr Singleton::m_pSingleton = std::shared_ptr<Singleton>(new Singleton);

Singleton::Ptr Singleton::getInstance() {
    return m_pSingleton;
}


int main() {
    Singleton::Ptr instance = Singleton::getInstance();
    instance->use();
    return 0;
    //运行结果
    //Constructor called!
    //in use
    //Destructor called!
}

总结

优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承(有资料说登记式可以继承,待确定!)

具体运用场景如:

你需要系统中只有唯一一个实例存在的类的全局变量的时候才使用单例。

  • 设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动; Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗?
  • 数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;
  • 日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加

参考资料

1.C++ 常用设计模式(学习笔记)
2.最推荐的懒汉式单例(magic static )——局部静态变量
3.C++ 单例模式总结与剖析
4.单例模式
5.探索单例模式



下面的是我的公众号二维码图片,欢迎关注。
图注:幼儿园的学霸

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值