C++ 单例模式

C++ 单例模式,第1张

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

单例模式作为最常用的设计模式之一,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。



单例特点:

1 、在任何情况下,单例类永远只有一个实例存在。


2 、单例需要有能力为整个系统提供这一唯一实例。


  C++实现单例模式 一般是将构造函数,拷贝构造函数 ,赋值运算符函数声明为私有的,从而禁止他人创建实例。


否则如果上面三者不为私有,那么他人就可以调用上面的三个函数来创建实例,我们可以提供一个public的静态方法来获得这个类唯一的一个实例化对象。


  单例模式一般存在两种实现模式

饿汉模式:

  即迫不及待,像一个饿汉一样,不管需不需要用到实例都要去创建实例,即在类产生的时候就创建好实例(实例的初始化放在getinstance函数外部,getinstance函数仅返回该唯一实例的指针)。


这是一种空间换时间的做法。


懒汉模式:

  即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化(实例的初始化放在 getinstance函数(getinstance只是一个函数名) 内部)经典的线程安全懒汉模式,使用双检测锁模式(p == NULL检测了两次),利用局部静态变量实现线程安全懒汉模式。


这是一种时间换空间的做法,这体现了“懒汉的本性”。



  
  
  我们先来看看 饿汉模式:
  饿汉模式的对象在类产生时候就创建了,一直到程序结束才会去释放。


即作为一个单例类实例,它的生存周期和我们的程序一样长。


因此该实例对象需要存储在全局数据区,所以肯定需要使用static来修饰,因为类内部的static成员是不属于每个对象的,而是属于整个类的。


在加载类的时候,我们的实例对象就产生了。


所以对于饿汉模式而言,是线程安全的,因为在线程创建之前实例已经被创建好了。



  
  我们模拟一个饿汉模式的单例类

#include 
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;
}
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 中实现的实例是一个类对象,现在我们看看来把它换成类对象的指针会怎么样

#pragma once
 
#include 
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 直接释放该实例是不可能的,因为它的析构函数是私有的,调不到析构函数(析构函数是私有也是我们要求的)。


  

方法一:在类中再写一个主动释放资源的方法
#include 
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;
}
 
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;
}


  但是这样的写法,常常会忘记手动去调用函数来释放资源,于是,我们想到了,可不可以有一个自动地能够释放资源的函数。




方法二:在类中定义一个内部的类来释放资源
#include 
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;   
}
 
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!当然了,我们想到,为什么不尝试用一用智能指针呢?智能指针不就是为了能够让我们不需要手动释放资源而设计的么,它会自动去释放资源啊?现在我们尝试着用智能指针试试创建一个单例类。


#include 
#include 
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;    
}

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可以指定删除器的特点,自定义删除器。


这一点是我之前不知道的,记录一下啊

#include 
#include 
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;
}



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()获取实例,那么可能就会产生多个实例,那么这就不是我们的单例模式了。


因此我们需要做一点事情,才能够避免这种情况的发生
  
  
  
分割线-----------------------------------------------------------

  
  
  

懒汉模式

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



  
  经典的线程安全懒汉模式,使用双检测锁模式(p == NULL检测了两次)
  利用局部静态变量实现线程安全懒汉模式
  
  如果有多个线程,同时调用了获取实例,那么可能就会产生多个实例,那么这就不是我们的单例模式了。


所以我们需要做一些事情才能保证我们是一个单例类。


当然,我们先来考虑是单线程的情况,再去考虑多线程的情况,我们总得先把懒汉模式的本质给弄明白。



  
  对于饿汉模式来说,它在单例类内部定义了一个类对象的指针,在初始化这个指针的时候,直接调用new在堆上申请了空间。


于是达到了这种效果,在进入main函数之前,这个单例类的实例已经创建好了。


  而对于懒汉模式而言,我们可不可以不让这个类对象指针在初始化的时候就new,而是给它赋一个NULL,那这样,在进入main函数之前,这个类对象指针只是一个空指针,并没有产生实际的对象。


而当我们在程序中调用getInstance函数时,就需要进行一个判断,如果该类对象指针为空,那么我们就需要调用new创建一个对象,而如果该类对象指针不为空,那么我们就不用创建对象直接返回该指针就好了。


根据这个思路,我们实现了下面的代码(因为我们的静态成员变量采用的是类对象的指针而不是类对象,因此我们需要写一个垃圾回收机制,这里我们采用的是上一篇文章的方法二,即实现一个内部的垃圾回收类)

#include 
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 = NULL; 
Singleton::Garbo Singleton::_garbo;     //还需要初始化一个垃圾清理的静态成员变量
 
Singleton* Singleton::getInstance(){
    if(instance == NULL)
        instance = new Singleton();
    return instance;   
}


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;
}


  可以看到我们先进入了main函数中,在调用了第一个getInstance获取实例的时候才创建了一个单例对象。


在调用后续的getInstance获取实例的时候因为instance不为空,所以直接返回instance。



  
  
当然了上面我们是在堆上创建了对象,想在栈上创建对象也是可以的
  

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

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;
}


  上面这两种情况在单线程的情况下,是没有问题。


但是试想在多线程的情况下,试想一下如果多个线程同时调用了getInstance函数,此时可能存在多线程竞态环境,就可能会产生重复构造或者是构造不完全等问题。


因此我们在getInstance函数中还要采用线程同步的方式,保证线程同步,从而达到单例类的目的
  
  
  
在堆上创建对象

#include 
#include 
#include 

pthread_mutex_t m_mutex;
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 = NULL; 
Singleton::Garbo Singleton::_garbo;     //还需要初始化一个垃圾清理的静态成员变量
 
Singleton* Singleton::getInstance(){
    pthread_mutex_lock(&m_mutex); // 加锁
    if(instance == NULL)
        instance = new Singleton();
    pthread_mutex_unlock(&m_mutex); // 解锁
 
    return instance;   
}


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;
}

  
  
  
在栈上创建对象

#include 
#include 
#include 

pthread_mutex_t m_mutex;
using namespace std;

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

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;
}


  Lock()和UnLock()原作者没有实现,这是一个加锁的步骤而已,都是常见的线程同步的方法。


我加入了pthread_mutex_unlock()的方法
  当两个线程同时想创建一个实例,由于在一个时刻只有一个线程能得到同步锁,当一个线程加上锁时,第二个线程只能等待,当第一个线程发现实例还没有创建时,它创建出第一个实例。


接着第一个线程释放同步锁,此时第二个线程可以加上同步锁,这个时候由于实例已经被第一个线程创建出来了,第二个线程就不会重复创建了,这样就保证了我们在多线程环境中也只能得到一个实例。


  但是问题就出来了,加锁和解锁的 *** 作是需要时间的,因此在线程很多的情况下,就会浪费大量的时间,导致效率下降。


下面我们可以用一种优化的方法(因为只改动了getInstance这个函数,于是我只把这个函数写了出来)。


Singleton* Singleton::getInstance(){
    if(instance == NULL){
        Lock(); // 加锁
        if(instance == NULL)
            instance = new Singleton();
        UnLock(); // 解锁
    }
    return instance;   
}

  上面这种方法只有在instance为空的时候才需要加锁和解锁 *** 作,如果已经创建出来了实例,则无需加锁。


所以上面的代码只有在第一个创建实例时才会需要加锁。


这样的方法效率已经很好了,只是实现起来比较麻烦,容易出错。



  
  
   懒汉模式适用于线程比较少的场景,因为线程一旦多,加锁的开销就会体现出来(当然最后对懒汉优化的那种方案已经差不多解决这个问题了),总之它是一种时间换空间的模式。



  饿汉模式适用于线程比较多的场景,它会占用全局静态区一定的空间,但是能够确保只有一个实例。


但是当线程很少,甚至是没有用到这个单例类的时候,就显得得不偿失了,它占用的空间问题就体现出来了,这是一种空间换时间的模式。



  
  
以上我参考了
单例模式——饿汉模式
单例模式——懒汉模式

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/584944.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-12
下一篇 2022-04-12

发表评论

登录后才能评论

评论列表(0条)

保存