单例模式的前世今生

前世今生

为什么要用单例模式

我所理解设计模式并非一种高端编程技巧。设计模式的本质还是在于更高效,更简单的解决问题。

所以如果你想出某个非常复杂,一般人难以理解的方式。那多半不适宜。


回到正题。使用单例模式的目的只有一个。
你想要在你的程序中全局的有且仅有一个的唯一对象。比如词典类,或者缓存等等。

单例模式和全局变量(或者是全局静态变量)

一般来说,是不用全局变量,而是全局静态变量。但在这里作对比时,一样考虑。
单例模式是全局变量的更好的替代品


全局变量

  1. 全局变量生命周期贯穿程序始终。
  2. 整个程序可见。即作用域是整个程序。

单例模式

  1. lazy模式可以避免耗时的全局变量初始化所导致的启动速度不佳等问题
  2. 整个程序可见
  3. 可以保证实例只有一个(这才是单例模式的主要目的)

写在前面的话

看了好多文章,给了好多单例模式的例子,由于文章所在时间原因,不同时期给出了不同例子,导致自己在学习时候比较困惑,学习完之后自己在这里试图解释,以及做出总结。很多文章谈了很多错误的例子,以及告诉为什么是错的。这里我就不想谈了,就直接以什么是对的,以及为什么这样做,实际上就能明白其他的为什么错了。

什么样的单例模式符合要求

而单例模式的要求则是

  1. 精简,就是不能太复杂,也不能很难理解。
  2. 线程安全
  3. 空间还是时间?(对应懒汉还是饿汉模式)
  4. 确保在使用期间,单例对象存在。
  5. 确保其仅仅创建了一个。
  6. 确保在程序结束时,自动释放资源。
  7. 提供一个全局的使用接口。

实际上,单例模式目前来说主流版本为俩大版本,不敢说是最正确的方式,但确实是最优雅的俩个版本。

前世——C++11之前的单例模式

lazy模式和hungry模式比较

单例模式分为懒汉模式和饿汉模式。
懒汉模式即为第一次使用时才实例化。保证了初始加载速度。如果实例没有使用,也不会浪费内存。
饿汉模式则是在main之前就实例化,因此不涉及线程安全。

构造函数和析构函数设置为非公有

构造函数和析构函数设置为非公有后,类的使用者就不可以构造类的对象。因为构造类的对象必须要调用构造函数。

  1. 在栈上,调用构造函数
  2. 在堆上,new会先分配空间,然后做指针类型转换,然后调用构造函数。

因此,构造函数不能使用之后,类的使用者将无法在类外创建对象。

设置为静态指针成员和静态成员函数

我们最终在类外使用,C++提供了静态成员函数可以根据类名直接调用静态成员函数的方法。而在静态成员函数内又只能使用静态数据成员。

为什么使用指针?

对于instance_来说,有三种实现方法。

  1. 一种是直接使用静态成员变量,但这样不是lazy模式了。
  2. 一种是指针,本文实现
  3. 一种是引用,但要在初始化列表进行初始化。
protected:
// 多写一个构造函数
Singleton(Singleton &other)
: instance_{other}
{}

static void init()
{
    static Singleton sins{};
    Singleton(sins);
    // atexit(destroy); // 但是不用在去管理对象的销毁
}

返回指针还是返回引用

指针和引用就语义最大的区别在于。指针需要delete,而且可以指向null。在使用指针时,我们总是认为它的回收需要用户来负责。而引用则不然,使用引用时则不然。

因此Singleton返回一个引用也就表示其生存期由非用户代码所管理。因此使用取址运算符获得指针后又用delete关键字删除Singleton所返回的实例明显是一个用户错误。

noncopyable

因为返回的是引用,所以不需要再处理operator&。对引用去&本身就是错误操作,因此没有必要再处理了。

C++11做法

public:
Singleton(const Singleton& other) = delete;
Singleton & operator=(const Singleton& other) = delete;
  1. 放在public下,可以让编译器更好地报错。

C++11之前做法

private:
Singleton(const Singleton& other) {};
Singleton & operator=(const Singleton& other) {};

线程安全

为了线程安全问题,之前有很多方式处理。
详细请看

  1. 在初始化时,使用互斥量。性能太低
  2. 使用DoubleCheck,后来证明其本身就是错误的
  3. pthread_once,我认为是最优雅的方式处理了。

atexit或者DeleteSingleton需要吗?

DeleteSingleton就是用一个子类静态成员,在程序结束前调用静态成员析构函数去处理,类似atexit功能。

本事实际上是多余的,因为main函数结束后才去delete指针。此时程序结束了,我们已经不用操这个心了。这里主要的目的是为了不被内存泄漏检查算法不要误报。

代码

class Singleton
{
public:
    static Singleton &getInstance()
    {
        pthread_once(&ponce_, Singleton::init);
        return *instance_; 
    }

    Singleton (const Singleton &other) = delete;
    Singleton & operator= (const Singleton &other) = delete;

    void run()
    {
        cout << "helloworld" << endl;
    }
    
protected:
    Singleton(){};
     ~Singleton(){};

private:
    static void init()
    {
        instance_ = new Singleton();
        atexit(destroy);
    }

    static void destroy()
    {
        if(instance_)
            delete instance_;
        instance_ = nullptr;
    }

    static Singleton * instance_;
    static pthread_once_t ponce_;
};
Singleton * Singleton::instance_ = nullptr;
pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;

int main()
{
    auto& tmp = Singleton::getInstance();
    tmp.run();
    return 0;
}

今生——C++11的单例模式

静态类型的lifetime

1. c语言的静态变量和c++的静态变量有点区别。

c和c++静态变量内存分配
C++标准规定:全局或静态对象当且仅当对象首次调用时才进行构造,并通过atexit()来管理对象的生命期。因此是在main结束前时调用相应的析构函数。
这解释了为什么静态成员变量需要定义。实际上是去调用构造函数。不然类没法知道这个静态成员的大小。


2. 线程安全

C++11

静态变量是线程安全的。用了memory barrier的方式,和memory_bodel有关,有点复杂,不想研究了。

这意味着我们的单例模式可以非常简单了。

C++11之前

非局部静态变量一般在main执行之前的静态初始化过程中分配内存并初始化,可以认为是线程安全的。
比如静态成员,全局静态变量。

局部静态变量在编译时,编译器的实现一般是在初始化语句之前设置一个局部静态变量的标识来判断是否已经初始化,运行的时候每次进行判断,如果需要初始化则执行初始化操作,否则不执行。这个过程本身不是线程安全的。

实际上就是一个非原子的

if(flag)
    intialize();

明显的线程不安全。比如类的函数内的局部静态变量。著名的例子就是单例模式中的局部静态锁。

BestofAll 代码

lazy

class Singleton
{
public:
    static Singleton& getInstance()
    {
        static Singleton instance;
        return instance;
    }
    
    Singleton(const Singleton&) = delete
    Singleton &operator=(const Singleton&( = delete;
    
protected:
    Singleton(){}
    ~Singleton(){}
};

hungry

class Singleton
{
public:
    static Singleton& getInstance()
    {
        return instance_;
    }
    
    Singleton(const Singleton&) = delete
    Singleton &operator=(const Singleton&( = delete;
private:
    static Singleton instance_;
protected:
    Singleton(){}
    ~Singleton(){}
};
Singleton Singleton::instance_;

重用——模板类

重用实际上就是写成模板类。
这里学自muduo库中的singleton。(不得不说,muduo库真是一个宝藏)
这里唯一需要注意的就是T必须是完整类了。比如单独的前向声明
class A;这就不是一个完整的类。完整的类必须有构造函数。才能导致new的时候不报错。

template <typename  T>
class Singleton
{
public:
    static T &getInstance()
    {
        pthread_once(&ponce_, Singleton::init);
        return *instance_; 
    }

    Singleton (const Singleton &other) = delete;
    Singleton & operator= (const Singleton &other) = delete;

protected:
    Singleton(){};
     ~Singleton(){};

private:
    static void init()
    {
        instance_ = new T();
        atexit(destroy);
    }

    static void destroy()
    {
        typedef char T_must_be_complete_type [sizeof(T) == 0 ? -1: 1];
        //T_must_be_complete_type dummy; (void) dummy;
        if(instance_)
            delete instance_;
        instance_ = nullptr;
    }

    static T * instance_;
    static pthread_once_t ponce_;
};
template<typename T>
T * Singleton<T>::instance_ = nullptr;
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;

本文参考

还有一些资料忘记标了。
[1] http://www.cnblogs.com/zxh1210603696/p/4157294.html
[2] http://www.cnblogs.com/loveis715/archive/2012/07/18/2598409.html
[3] https://stackoverflow.com/questions/86582/singleton-how-should-it-be-used
[4] https://stackoverflow.com/questions/1008019/c-singleton-design-pattern
[5] https://www.zhihu.com/question/24301047/answer/83422523

猜你喜欢

转载自blog.csdn.net/weixin_43468441/article/details/88872280