C++创建型设计模式之 单例模式

本文详细介绍了C++中单例模式的原理、实现方法,包括构造函数私有化、异常处理以及线程安全的获取实例策略。同时讨论了单例模式的适用场景、存在的问题和替代方案。
摘要由CSDN通过智能技术生成

C++创建型设计模式之 单例模式

单例模式简介

单例模式就是在类的设计时,保证只能生成一个该类型的实例。比如在与硬件通信接口、数据库交互、全局参数处理等。

单例类的实现方法

对于单例类的实现,无疑就两点需要注意,一个是阻止用户直接实例化,另一个是提供接口让用户来获取到这个惟一的实例。

阻止用户直接实例化方法

1、 最常用的方法将构造函数私有化。提一下,在C++类中,如果不显示的写出构造函数,那么编译时会有个默认的公有化的构造函数。

class Dog {
    Dog();
public:
    static Dog& Instance();
    void setColor();
    std::string getColor();
    //....
};

构造函数私有化后,用户如果直接实例化该类(Dog dog; 或 new Dog等)时,编译便会报错。这时候用户就不得不跳转到类本身进行查看原因。下面的Instance()函数为用户获取实例的接口,下面会对实现的讲述。

2、再次构造时抛异常方法。这个方法真是太不友好了,如果没有接异常那么整个软件都崩了。就算是接了异常,紧临着实例对象下边的代码也是不会执行了。

class Dog {
public:
    Dog()
    {
        static int instanceCount{ 0 };
        if (++instanceCount > 0)
            throw std::exception("instance has created!");
    }
    //....
};

这种方法达到了我们的目的,阻止了实例的重复构造,但尽量避免使用。

单例更严格的限制

如果不禁用掉拷贝构造,移动构造之类,那么用户还是可以通过拷贝或移动来获取到新的实例的。即然拷贝构造都禁用了,那么等号重载也禁用掉不过份吧?

class Dog
{
    Dog();
public:
    static Dog& Instance();
    Dog(const Dog&) = delete;
    Dog(Dog&&) = delete;
    Dog& operator=(const Dog&) = delete;
    Dog& operator=(Dog&&) = delete;
};

注:继承自boost::noncopyable 的类内部便已经有了拷贝函数的禁用。

还有一种情况就是用户继承单例类,再重写方法时,这便不再受限于原始设计者的限制了。
1、构造函数私有(不是保护类型)是阻止继承重写的有效方法,子类在构造时,由于不能构造基类从而无法实现类型的有效构造。
2、构造函数私有的变种。

template <class T>
class Base
{
    friend T;//模板参数类型作为友元函数
private:
    Base()
    {}
    ~Base()
    {}
};
//FinalClass作为Base的友元,可以访问Base的私有成员
class Dog:virtual public Base<MyClass>
{
    Dog();
publicstatic Dog& Instance();
    Dog(const Dog&) = delete;
    Dog(Dog&&) = delete;
    Dog& operator=(const Dog&) = delete;
    Dog& operator=(Dog&&) = delete;
};

参考:https://blog.csdn.net/ArchyLi/article/details/78567876#/

由于Dog类是基类的友元,可以访问父类私有的构造函数,可以完成类的构造,但其子类不行。
3、使用关键词 final 来阻止继承重写, 这样能清晰明了的阻止继承重写

class Dog final
{
    Dog();
public:
    static Dog& Instance();
    Dog(const Dog&) = delete;
    Dog(Dog&&) = delete;
    Dog& operator=(const Dog&) = delete;
    Dog& operator=(Dog&&) = delete;
};

获取实例接口的写法

获取的实例可以是引用,也可以是指针类型,如果是指针类型,这个指针也需要是全局类型的,但在构造时需要注意内存分配时的多线程造成多个实例问题

Dog* Dog::pDog = nullptr;
class Dog final
{
    Dog();
    static Dog* pDog;
public:
    static Dog* Instance();
};
Dog *Dog::Instance()
{
    if (!pDog)
        pDog = new Dog;
    return pDog;
}

这里的Instance()函数体内,如果指针为空,就现new个对象再进行返回,如果在new的过程中,另一个线程也进来了,那就可能造成返回的实例并不是同一个了,所以这里需要加锁限制下。

//方法1:接口内全局锁
Dog *Dog::Instance()
{
    std::lock_guard<std::mutex> lock(mtx);
    if (!pDog)
        pDog = new Dog;
    return pDog;
}
//方法2:双检查锁
Dog *Dog::Instance()
{
    if (!pDog) {
        std::lock_guard<std::mutex> lock(mtx);
        if (!pDog) pDog = new Dog;
    }
    return pDog;
}

方法1简单暴力,但效率变低,每次获取实例时都要检测锁,实际上只有在首次调用此接口,在实例化对象时才需要使用锁来避免同时构造两个对象。所以我们尽量采用方法2。因为方法2在获取到锁后,还要再次判空所以称为双检查,这样就能有效的避免一个线程正在创建时,另一个线程也进来了,然后创建了两个实例。

上面的设计中,我们多次提到只能执行一次,那么我容易想到标准库里的std::call_once,它能有效阻止某段化码的重复执行。但std::call_once 使用的标志一定要是各线程共享的。

//方法3:使用标准库接口
std::once_flag flag;
Dog *Dog::Instance()
{
    std::call_once(flag, [&] {pDog = new Dog; });
    return pDog;
}

单例的接口返回值并不是非要为指针型的,引用类型也很常用

Dog &Dog::Instance()
{
   static Dog dog;
    return dog;
}

如果是每个线程一个单例,那么static 换成 thread_local 即可,这种需求有点类似在新启线程里设个临时变量一样,所以使用也并不是很常见。

相关拓展

使用单例可能只是我们希望在软件的各处,使用的都是同一个实例对象,对于小团队的开发,就算我们不加创建实例上的限制,我们也是可以在软底层的地方设置个变量,然后其它使用的地方都以引用方式传过去肯定也是可以的。对于简单的数据,在类中写成static类型,或定义为全局类型,每个类型的实例也是共用一套数据。单例也并不是非要一五一十的写,按需写就行了。

单例模式存在的问题

1、单例模式对测试不太友好,特别是硬件通信接口的单例中,每次调用就真的需要连接上硬件才行。
2、想借用单例中的部分代码与其它共享,也是比较困难的,所以最好把单例的业务内容最大可能的做压缩限制,再用业务处理类进行封装。

参考书籍:《Design Patterns in Modern C++20》 Dmitri Nesteruk

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值