C++笔记---对于单例模式的相关讨论

C++笔记---对于单例模式的相关讨论,第1张

前言

单例模式是程序设计中一种非常常见的设计模式,在面向对象编程的时候,对于某一个类的实例对象,如果我们为了不频繁的创建和销毁它并且全局都使用这一个实例,那么我们就可以将其设计为单例模式,单例模式在实际应用场景还是比较多的,比如我们使用的鼠标箭头在系统运行过程中只能有一个,再比如我们设计登录框按钮的时候,即使多次单击也必须出现一个登录框实例。


单例模式所涉及的知识点当前先将其归纳为3点:

  • 单例模式最基本的设计方法 既然我们要保证将class设计成单例模式,那么其必然利用了一定的设计方法才能保证当创建多个对象实例的时候保证返回的是同一个实例,而且也有一些明显的设计特征。


  • 线程安全问题 这个问题是很自然而然的,由于其是单例模式,也就是整个程序运行周期只有一个实例,那么如果我两个甚至多个线程同时创建第一个实例的时候(因为必须得有第一个实例被创建出来),到底如何确保这个实例“唯一”呢?
  • 资源的回收问题 这个问题其实并不算单例模式特有的问题,但是对于这多个用户同时使用的“仅有”的一个实例如果结束生命周期之后如何对其申请的资源进行合理的释放也是值得关注的一个点。


单例模式最基本的设计方法

根据前面的描述,单例模式的类的设计应该有以下几个原则:只能有一个实例,类自行创建这个实例,向外界提供创建的这个实例。


至于为什么需要自行创建呢?如果我们允许用户使用 类名:对象名的创建方式创建实例,那么肯定不能保证“单例”,这也就是说我们不能给予用户构造函数的访问权限,构造函数必须私有;如何向外界提供这个实例呢?用户不能使用构造函数构造对象,所以只能使用类的静态方法得到对象,因为不能通过非静态方法得到对象,只能通过静态方法得到,众多周知,静态方法属于类而不属于某个具体的对象。


通过上面的讨论,我们可以得到单例模式的两个必要特征:构造函数私有化&&通过静态函数提供实例。


由于类通过静态函数往外界传送对象,那么类中必须定义一个静态的、指向自身的对象指针作为类中的成员变量

class SingleInstance{
private:
    SingleInstance(){}                      //构造函数私有化,不能别外界访问
    static SingleInstance* _single;         //指向自身的静态对象指针成员变量
public:
	...
    static SingleInstance* getInstance(){   //通过静态成员函数向外界提供实例
        return _single;
    }
    ...
};

基本的设计原则就如上所示,总结一下就是:

  • 构造函数私有化
  • 指向自身的静态对象指针成员变量
  • 通过静态成员函数向外界提供实例

那么既然有了指向自身的静态对象指针成员变量_single,那么何时为其分配内存构造出真正的对象实例呢?这里就引出了两种实现方式:懒汉式和饿汉式,也就是我们等到使用到这个实例的时候再去构造还是先构造出来以备不时之需呢?

饿汉式

饿汉式顾名思义就是比较饥饿,肯定想先把实例构造出来,代码如下:

class SingleInstance{
private:
    SingleInstance(){}                      //构造函数私有化,不能别外界访问
    static SingleInstance* _single;         //指向自身的静态对象指针成员变量
public:
	...
    static SingleInstance* getInstance(){   //通过静态成员函数向外界提供实例
        return _single;
    }
    ...
};
//类外初始化要带着类型SingleInstance*
SingleInstance* SingleInstance::_single = new SingleInstance();

这里有一个小点要注意一下:静态成员变量的初始化要放在类外

饿汉式设计方式的好处是类一被加载实例就被构造出来,这样就不用担心多线程都去获取实例而造成创建多个实例的问题了,但是这样不符合编程规范,因为如果从始至终没使用这个实例,那么就白白浪费了内存空间。


懒汉式

懒汉式的意思就是我比较懒,啥时候需要的时候我再去办,也就是在外界请求获取实例的时候再去创建实例,程序设计如下:

class SingleInstance{
private:
    SingleInstance(){}                      //构造函数私有化,不能别外界访问
    static SingleInstance* _single;         //指向自身的静态对象指针成员变量
public:
	...
    static SingleInstance* getInstance(){   //通过静态成员函数向外界提供实例
        if(_single == nullptr)
                _single = new SingleInstance();
        return _single;
    }
    ...
};
SingleInstance* SingleInstance::_single = nullptr;

懒汉式的设计方式中,实例_single是在getInstance()函数中创建的,因为外界获取实例需要通过getInstance获取,也就是在外界需要的时候再去构建,避免内存的浪费。



上面的代码在单线程中是没有问题,因为不会存在两个用户同时去调用getInstance函数造成同时构造实例两次,那样的话就不符合单例的要求了,不过在多线程中上面的代码风险就大了,这就是将要讲的第二部分的内容,线程安全的问题。


线程安全问题

对于单例模式而言,在多线程容易出现的问题就是如果多个线程同时第一次想要获取对象实例,可能会出现生成多个实例的情况,这样就不是单例模式了,所以我们应该采取安全措施避免这种情况。


对于饿汉式而言,由于第一次加载类的时候就将实例对象构造出来了,因此不存在这个问题,所以这里我们讨论的是懒汉式的设计方式如何有效的保证线程安全问题。


最基本的想法是为了避免多线程“同时”创建,那么我们就用互斥锁嘛,只有获得锁的线程有资格第一次创建实例,那我们就加锁试试看:

class SingleInstance{
private:
    static SingleInstance* _single;
    static mutex m_mutex;
    SingleInstance(){}
public:
static SingleInstance* getInstance(){
        if(_single == nullptr){
            unique_lock lock(m_mutex);  //此处加锁
             _single = new SingleInstance();
        }
        return _single;
    }
};
SingleInstance* SingleInstance::_single = nullptr;
mutex SingleInstance::m_mutex;   

可以看到,我们在类的成员变量里加了一个静态的互斥锁 static mutex m_mutex,然后在getInstance()函数中先判断一下_single是否是空,如果是空的话那么就获得当前锁用来构建对象实例,因为是互斥锁,所以同一时刻只能有一个线程获得锁去创建实例,看起来好像没有什么问题,挺符合单例模式的原则的,但是就按照上面的代码让我们分析一下:
假设线程A、B、C同时调用getInstance()函数想获得对象实例,并且由于多线程可并行,都通过了代码 if(_single == nullptr)的检验,此时三个线程来到了“抢锁”的节点,我们假设B获得了锁,那么A和C就被阻塞在此等待B用完释放,B获得锁使用就进入了构建实例对象的部分,构建完之后就把锁释放了,注意此时对象已经被构建出来了,然后再假设A获得了B释放的锁由阻塞状态转为运行状态,由于之前通过了对象指针的非空的判断,因此A接下来竟然也要去创建对象!而且之后的C也要去创建对象!所以即使我们按照上述代码加锁,依然有可能存在创建多个实例的情况,而问题的根源就在于当线程B创建了对象之后,A和C在解除阻塞态的时候并不知道对象已经被创建出来了,所以我们应该让A和C知道是否对象实例已经被创建出来,也就是在获得锁之后再加一个判断来判断对象指针是否为空,这也就是所谓的双重验证,代码很简单,只需增加一个条件语句即可:

class SingleInstance{
private:
    static SingleInstance* _single;
    static mutex m_mutex;
    SingleInstance(){}
public:
static SingleInstance* getInstance(){
        if(_single == nullptr){
            unique_lock lock(m_mutex);  //此处加锁
             if(_single == nullptr)   //判断是否线程在阻塞的时候其他线程已经构建了对象实例
             	_single = new SingleInstance();
        }
        return _single;
    }
};
SingleInstance* SingleInstance::_single = nullptr;
mutex SingleInstance::m_mutex;   

这样,当线程B获得锁构建了对象实例再释放锁,A再获得锁之后,首先进行非空判断,发现对象已经被构造出来了,那么就直接释放掉锁,返回已经构建的对象实例即可,对于C而言也是如此。


资源的回收问题

在上面的代码中,包含懒汉式和饿汉式设计方式,我们都使用了_single = new SingleInstance()来为对象动态分配内存, 这里要注意,由于使用了new动态申请内存空间,那么必然需要delete来手动释放,而使用delete的本质就是去调用对象的析构函数, 如果没有delete,那么对象实例的析构函数就调用不了,内存释放不了,就存在了内存泄漏的问题。


比如下面代码:

class SingleInstance{
private:
    //静态成员变量一定要在类外初始化
    static SingleInstance* _single;
    static mutex m_mutex;
    SingleInstance(){std::cout<<"实例创建"< lock(m_mutex);
            if(_single == nullptr)
                _single = new SingleInstance();
        }
        return _single;
    }
SingleInstance* SingleInstance::_single = nullptr;
mutex SingleInstance::m_mutex;

int main()
{
	SingleInstance* s1 = SingleInstance::getInstance();
	SingleInstance* s2 = SingleInstance::getInstance();
	return 0;
}

上述代码的打印结果是:实例创建。


也就是说没有调用析构函数打印 实例销毁,这是因为没有delete去手动释放动态申请的资源。


所以下面就重点讨论资源释放的问题,不过这里要明确一点,出现上述问题的原因是因为我们使用new申请了资源,对象的成员变量 _single仅仅是一个地址,对象真正的资源是放在堆上,所以需要delete释放,如果直接使用SingleInstance singleinstance构建对象,在程序结束的时候就能够自动调用析构函数了,切记!!!

接下来我们主要从3个思路解决这个问题:

  • 既然需要手动释放,那我定义一个静态函数手动调用就可以了
  • 类的析构函数在生命周期结束后自动调用析构函数,可不可以利用这个特性呢?
  • C++11中的智能指针就是解决new的资源释放问题的,可不可以利用一下呢?
定义函数手动释放

这个方案是没问题的,不过我们要注意:定义的函数需要是静态的,然后在程序结束之前调用它,手动执行delete释放内存,函数定义如下:

static void deleteInstance(){
        if(_single != nullptr){
            unique_lock lock(m_mutex);
            if(_single != nullptr){
                delete _single;
                _single = nullptr;
            }
        }
    }

这里也要注意几个点:线程保护问题,使用双重验证解决多线程中资源重复释放的问题,具体理解参见上一节;资源释放之后,将指针指向空 _single = nullptr这是一个好的习惯。



但是手动释放不是一个好的解决方式,因为客户可能不一定每次都记住自己需要手动释放资源,如果忘记了,也很容易造成资源的泄漏。


利用类对象在生命周期结束后自动调用析构函数的特性

其实这一个方法和下面所要讨论的使用智能指针的方法,本质上都是RALL的程序编写理念,资源获取及初始化,核心是将资源与对象的生命周期绑定在一起:对象初始化的时候分配资源,对象生命周期结束的时候销毁资源。



那么我们如何利用类对象在生命周期结束的时候自动调用析构函数的特性来释放单例模式下对象实例的资源释放呢?首先我们肯定不能用SingleInstance类了,我们需要再额外定义一个class并实例化一个对象,他需要有以下特点:

  • 这个对象能够访问到SingleInstance的成员变量,访问到才能delete
  • 这个对象在程序结束的时候系统能自动调用析构函数,所以他的位置应该放在全局区要和SingleInstance的生命周期一样

因此,我们可以在SingleInstance内部声明一个嵌套类,并且实例化一个静态的类对象,代码如下:

class SingleInstance{
private:
    //静态成员变量一定要在类外初始化
    static SingleInstance* _single;
    static mutex m_mutex;
    SingleInstance(){std::cout<<"实例创建"< lock(m_mutex);
                    	if(SingleInstance::_single != nullptr){
                        	delete SingleInstance::_single;
                        	SingleInstance::_single = nullptr;
                        }
                    }
                }
    };
    static Garbo garbo;       //这里要设置为静态的
    
public:
static SingleInstance* getInstance(){
        if(_single == nullptr){
            unique_lock lock(m_mutex);
            if(_single == nullptr)
                _single = new SingleInstance();
        }
        return _single;
    }
};
SingleInstance* SingleInstance::_single = nullptr;
mutex SingleInstance::m_mutex;
SingleInstance::Garbo SingleInstance::garbo;

上面的代码是能够在程序运行结束的时候自动释放_single 所指向的内存空间的,他的原理是这样的:在程序结束的时候,系统会自动析构所有的全局变量,事实上系统也会析构所有类的静态成员变量(堆上的变量不可以),而garbo就是类中的静态成员变量,当程序结束的时候,他的析构函数被调用,在析构函数里会用双重验证判断_single 是否别释放掉,如果没有就delete掉内存空间,最终可以完成_single 内存空间的自动释放。


利用C++11中的智能指针

其实这个想法是很自然而然的,因为智能指针的出现有很大一部分原因就是为了解决人们可能记不住deletenew出来的内存空间,那么我们遇到的问题也是因为这个原因,那么使用智能指针就很合乎情理了,因此我们将懒汉式的设计方式中的_single替换为智能指针的形式,代码如下:

class SingleInstance{
private:
	static shared_ptr _single;
    static mutex m_mutex;
    SingleInstance(){std::cout<<"实例创建"< getInstance(){
        if(_single == nullptr){
            unique_lock lock(m_mutex);
            if(_single == nullptr)
                _single = shared_ptr(new SingleInstance());
        }
        return _single;
    }
};
shared_ptr SingleInstance::_single = nullptr;
mutex SingleInstance::m_mutex; 

int main(){
	shared_ptr s1 = SingleInstance::getInstance();
	shared_ptr s2 = SingleInstance::getInstance();
	shared_ptr s3 = SingleInstance::getInstance();
	return 0;
}

上面代码的思路很清晰,就是想利用智能指针计数为0的时候自动调用对象的析构函数,但是理想很丰满现实很骨感,上面的代码是编译不通过的,因为对于shared_ptr来说,其计数为0的时候是通过调用对象的析构函数去释放内存,但是在单例模式中析构函数被设计为private,所以shared_ptr调用不到,也就编译不过去。



当然可以将析构函数设计为public,不过这样不好,因为这样客户会被给予调用析构函数的权限,如果调用了析构函数,那么会出现问题,我在网上学到的一种解决办法是:自定义shared_ptr的删除器函数,这里使用shared_ptr的另一种构造函数形式:share_prt(Y* p, D d),其中p就是传入的用来构造智能指针的对象指针,而d则是智能指针计数为0的时候,对p进行如何的删除 *** 作,之前是delete p,现在是d(p),这里的d就是一个回调函数,其参数是Y* p



所以现在的思路是:在类内实现一个释放内存资源的静态成员函数,将此函数作为构造智能指针时的删除器,代码如下:

class SingleInstance{
private:
	static shared_ptr _single;
    static mutex m_mutex;
    SingleInstance(){std::cout<<"实例创建"< lock(m_mutex);
			if(p != nullptr){
				delete p;
				p == nullptr;
				std::cout<<"此处销毁"< getInstance(){
        if(_single == nullptr){
            unique_lock lock(m_mutex);
            if(_single == nullptr)
                _single = shared_ptr(new SingleInstance(), SingleInstance::distroy);
        }
        return _single;
    }
};
shared_ptr SingleInstance::_single = nullptr;
mutex SingleInstance::m_mutex; 

int main(){
	shared_ptr s1 = SingleInstance::getInstance();
	shared_ptr s2 = SingleInstance::getInstance();
	shared_ptr s3 = SingleInstance::getInstance();
	return 0;
}

打印结果:
实例创建
实例销毁
此处销毁

此处的运行分析是:当shared_ptr _single的计数为0时,调用指定的删除器函数distroy( _single.get()),注意这里删除器传入的参数是构造智能指针的对象指针指针并不是智能指针本身,然后在里面执行了delete *** 作调用到了析构函数,打印出实例销毁,然后又打印了删除器函数内的此处销毁


知识点总结

上面是按照方法进行讨论的,这里就简单总结一下用到的C++中的知识点:

  • 通过new分配的资源是在堆上的,这部分资源系统不负责释放,而是需要delete手动释放,delete的本质是调用对象的析构函数,所以不要在本类的析构函数中使用delete释放本类,造成死循环
  • 类中的静态成员对象在生命周期结束后系统会负责释放其内存资源,这时候可以利用其析构函数做一些自动化的 *** 作
  • 智能指针shared_ptr释放内存的本质是使用delete释放,不过我们可以在构造智能指针的时候传递删除器函数,按照自定义 *** 作释放资源,替换delete
  • C++的类中可以定义静态的自身对象,不能定义非静态的自身对象,但是可以定义非静态的自身对象的引用和指针。


    这是因为静态的自身对象不占对象的内存,而非静态的自身对象的引用和指针占固定大小的内存(本质上都是指针),非静态的自身对象就不可以了,比如定义一个对象为其分配空间,空间里面又有一个对象,又为其分配空间…周而复始循环下去,这是不可以的。


参考:
https://wenku.baidu.com/view/007a6b25ecf9aef8941ea76e58fafab069dc4421.html
https://blog.csdn.net/weixin_41176628/article/details/117674462
https://www.cnblogs.com/cxjchen/p/3148582.html

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

原文地址: http://outofmemory.cn/langs/634483.html

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

发表评论

登录后才能评论

评论列表(0条)

保存