单例模式——饿汉模式

所谓的单例模式,就是设计一个类,它在整个程序中只能有一个该类的实例存在,这就是单例模式。

C++实现单例模式的一般方法是将构造函数,拷贝构造函数以及赋值运算符函数声明成private,从而禁止他人创建实例。否则如果上面三者不为私有,那么他人就可以调用上面的三个函数来创建实例,就没法实现单例模式。但是我们总归是要创建一个类的,我们可以提供一个public的静态方法来帮助我们获得这个类唯一的一个实例化对象

单例模式一般有两种实现模式

  • 饿汉模式:像一个饿汉一样,不管需不需要用到实例都要去创建实例,即在类产生的时候就创建好实例,这是一种空间换时间的做法。作为一个饿汉而言,体现了它的本质——“我全都要”。 

  • 懒汉模式:像一个懒汉一样,需要用到创建实例了程序再去创建实例,不需要创建实例程序就“懒得”去创建实例,这是一种时间换空间的做法,这体现了“懒汉的本性”。

在这篇文章中,我们暂且只来讨论饿汉模式

       饿汉模式的对象在类产生时候就创建了,一直到程序结束才会去释放。即作为一个单例类实例,它的生存周期和我们的程序一样长。因此该实例对象需要存储在全局数据区,所以肯定需要使用static来修饰,因为类内部的static成员是不属于每个对象的,而是属于整个类的。在加载类的时候,我们的实例对象就产生了。所以对于饿汉模式而言,是线程安全的,因为在线程创建之前实例已经被创建好了。

下面我们可以来模式实现一个饿汉模式的单例类

singleton.hpp

#pragma once

#include <iostream>
using namespace std;

class Singleton{
private:
    Singleton(){
        cout << "创建了一个单例对象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        //析构函数我们也需要声明成private的
        //因为我们想要这个实例在程序运行的整个过程中都存在
        //所以我们不允许实例自己主动调用析构函数释放对象
        cout << "销毁了一个单例对象" << endl;
    }


    static Singleton instance;  //这是我们的单例对象,注意这是一个类对象,下面会更改这个类型
public:
    static Singleton* getInstance();
};

//下面这个静态成员变量在类加载的时候就已经初始化好了
Singleton Singleton::instance; 

Singleton* Singleton::getInstance(){
    return &instance;
}

test.cc

#include "singleton.hpp"

int main(){
    cout << "Now we get the instance" << endl;
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    Singleton* instance3 = Singleton::getInstance();
    cout << "Now we destroy the instance" << endl;
    return 0;
}

我们来看下运行结果

由此可见,对于我们的程序,在我们输出Now we get the instance这句话的时候,也就是我们即将获取到这个类对象的实例的时候,在这之前这个单例类的实例在加载类的时候已经被创建好了,且我们调用了三个getInstance来获取实例,也并没有因此而多创建更多的实例,因此它是一个单例类。在程序结束的时候,这个唯一的实例才会被销毁。

上面我们在singleton.hpp 中实现的实例是一个类对象,现在我们看看来把它换成类对象的指针会怎么样

singleton.hpp

#pragma once

#include <iostream>
using namespace std;

class Singleton{
private:
    Singleton(){
        cout << "创建了一个单例对象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        //析构函数我们也需要声明成private的
        //因为我们想要这个实例在程序运行的整个过程中都存在
        //所以我们不允许实例自己主动调用析构函数释放对象
        cout << "销毁了一个单例对象" << endl;
    }
    static Singleton* instance;  //这是我们的单例对象,它是一个类对象的指针
public:
    static Singleton* getInstance();
};

//下面这个静态成员变量在类加载的时候就已经初始化好了
Singleton* Singleton::instance = new Singleton(); 

Singleton* Singleton::getInstance(){
    return instance;    //这里就是直接返回instance了而不是返回&instance
}

测试程序不变,我们再来看一下程序运行结果

 这是怎么回事???程序居然没有调用析构函数??那么会有什么后果?没有释放占有的资源,导致内存泄漏!!

这是为什么呢?因此此时全局数据区中,存储的并不是一个实例对象,而是一个实例对象的指针,它是一个地址而已,我们真正占有资源的实例对象是存储在堆中的。这样的声明方法可以减小全局数据区的占用量,把一大堆单例对象放在了堆中,但我们需要主动地去调用delete释放申请的资源。我们想要手动调用delete 直接释放该实例是不可能的,因为它的析构函数是私有的,调不到析构函数(析构函数是私有也是我们要求的)。

delete Singleton::getInstance;    //这编译不过,调不到析构函数

方法一:在类中再写一个主动释放资源的方法

singleton.hpp

#pragma once

#include <iostream>
using namespace std;

class Singleton{
private:
    Singleton(){
        cout << "创建了一个单例对象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        //析构函数我们也需要声明成private的
        //因为我们想要这个实例在程序运行的整个过程中都存在
        //所以我们不允许实例自己主动调用析构函数释放对象
        cout << "销毁了一个单例对象" << endl;
    }
    static Singleton* instance;  //这是我们的单例对象,它是一个类对象的指针
public:
    static Singleton* getInstance();
    static void deleteInstance();   //用来销毁实例
};

//下面这个静态成员变量在类加载的时候就已经初始化好了
Singleton* Singleton::instance = new Singleton(); 

Singleton* Singleton::getInstance(){
    return instance;   
}
void Singleton::deleteInstance(){
    delete instance;
}

test.cc

#include "singleton.hpp"

int main(){
    cout << "Now we get the instance" << endl;
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    Singleton* instance3 = Singleton::getInstance();
    cout << "Now we destroy the instance" << endl;
    Singleton::deleteInstance();
    return 0;
}

程序运行结果如下图

但是这就让很多程序员不爽了,因为他们(哈哈当然还有我)总是会忘记手动去调用函数来释放资源。于是我们想到了,可不可以有一个自动地能够释放资源的函数。

方法二: 定义一个内部的类

singleton.hpp

#pragma once

#include <iostream>
using namespace std;

class Singleton{
private:
    Singleton(){
        cout << "创建了一个单例对象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        //析构函数我们也需要声明成private的
        //因为我们想要这个实例在程序运行的整个过程中都存在
        //所以我们不允许实例自己主动调用析构函数释放对象
        cout << "销毁了一个单例对象" << endl;
    }

    static Singleton* instance;  //这是我们的单例对象,它是一个类对象的指针
public:
    static Singleton* getInstance();

    
private:
    //定义一个内部类
    class Garbo{
    public:
        Garbo(){}
        ~Garbo(){
            if(instance != NULL){
                delete instance;
                instance = NULL;
            }
        }
    };

    //定义一个内部类的静态对象
    //当该对象销毁的时候,调用析构函数顺便销毁我们的单例对象
    static Garbo _garbo;
};

//下面这个静态成员变量在类加载的时候就已经初始化好了
Singleton* Singleton::instance = new Singleton(); 
Singleton::Garbo Singleton::_garbo;     //还需要初始化一个垃圾清理的静态成员变量

Singleton* Singleton::getInstance(){
    return instance;   
}

test.cc

#include "singleton.hpp"

int main(){
    cout << "Now we get the instance" << endl;
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    Singleton* instance3 = Singleton::getInstance();
    cout << "Now we destroy the instance" << endl;
    return 0;
}

程序运行结果如下

我们成功地销毁了对象,而且还没有手动去释放!Perfect!当然了,我们想到,为什么不尝试用一用智能指针呢?智能指针不就是为了能够让我们不需要手动释放资源而设计的么,它会自动去释放资源啊?关于智能指针的问题,不懂的朋友去看一看我的另一篇博客: C++中的智能指针。现在我们尝试着用智能指针试试创建一个单例类。

singleton.hpp

#pragma once

#include <iostream>
#include <memory>
using namespace std;

class Singleton{
private:
    Singleton(){
        cout << "创建了一个单例对象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        //析构函数我们也需要声明成private的
        //因为我们想要这个实例在程序运行的整个过程中都存在
        //所以我们不允许实例自己主动调用析构函数释放对象
        cout << "销毁了一个单例对象" << endl;
    }

    static shared_ptr<Singleton> instance;  //这是我们的单例对象,它是一个类对象的指针
public:
    static shared_ptr<Singleton> getInstance();
};
    

//下面这个静态成员变量在类加载的时候就已经初始化好了
shared_ptr<Singleton> Singleton::instance(new Singleton()); 

shared_ptr<Singleton> Singleton::getInstance(){
    return instance;    
}

test.cc

#include "singleton.hpp"

int main(){
    cout << "Now we get the instance" << endl;
    shared_ptr<Singleton> instance1 = Singleton::getInstance();
    shared_ptr<Singleton> instance2 = Singleton::getInstance();
    shared_ptr<Singleton> instance3 = Singleton::getInstance();
    cout << "Now we destroy the instance" << endl;
    return 0;
}

你会发现它甚至连编译都过不了,原因是shared_ptr无法访问私有化的析构函数。但是我们又需要析构函数是私有的,这就矛盾起来了(为什么希望析构函数是私有的如上注释)。因此,我们就需要用到shared_ptr可以指定删除器的特点,自定义删除器。

方法三:shared_ptr与自定义删除器

singleton.hpp

#pragma once

#include <iostream>
#include <memory>
using namespace std;

class Singleton{
private:
    Singleton(){
        cout << "创建了一个单例对象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        //析构函数我们也需要声明成private的
        //因为我们想要这个实例在程序运行的整个过程中都存在
        //所以我们不允许实例自己主动调用析构函数释放对象
        cout << "销毁了一个单例对象" << endl;
    }
    static void DestroyInstance(Singleton*);    //自定义一个释放实例的函数

    static shared_ptr<Singleton> instance;  //这是我们的单例对象,它是一个类对象的指针
public:
    static shared_ptr<Singleton> getInstance();
};
    

//下面这个静态成员变量在类加载的时候就已经初始化好了
shared_ptr<Singleton> Singleton::instance(new Singleton(),Singleton::DestroyInstance); 

shared_ptr<Singleton> Singleton::getInstance(){
    return instance;    
}
void Singleton::DestroyInstance(Singleton*){
    cout << "在自定义函数中释放实例" << endl;
}

test.cc

#include "singleton.hpp"

int main(){
    cout << "Now we get the instance" << endl;
    shared_ptr<Singleton> instance1 = Singleton::getInstance();
    shared_ptr<Singleton> instance2 = Singleton::getInstance();
    shared_ptr<Singleton> instance3 = Singleton::getInstance();
    cout << "Now we destroy the instance" << endl;
    return 0;
}

程序的运行结果如下

好了,饿汉模式的单例类讲完了,因为单例模式在程序一开始就初始化好实例,所以后续不再需要考虑线程安全的问题,因此它适用于线程比较多的程序中,以空间换取时间,提高了效率。

但在懒汉模式中,情况就不一样了,因为它是在使用时才创建实例,在第一次调用getInstance()的时候,才创建实例对象。如果有多个线程,同时调用了getInstance()获取实例,那么可能就会产生多个实例,那么这就不是我们的单例模式了。因此我们需要做一点事情,才能够避免这种情况的发生。可以看看我的下一篇文章单例模式的懒汉模式

猜你喜欢

转载自blog.csdn.net/lvyibin890/article/details/81943637