C++实现懒汉式单例模式

单例模式无论在生活中还是在工程中都有很广泛的应用,在C++项目中,很多时候我们只希望整个工程中某个类仅有一个实体对象,设计这种类的时候就需要使用单例模式来设计。下面是实现的一个懒汉式单例模式的代码:

#include <iostream>
#include <cstdio>

using std::cout;
using std::endl;

class Singleton {
    private :
        static Singleton* p_obj;
        Singleton(void){

        }
    public :
        static Singleton* GetInstance(void){
            if(p_obj == NULL){
                p_obj = new Singleton();
                cout << "New Singleton();" << endl;
                return p_obj;
            }
        }

        ~Singleton(void){
            if(p_obj!=NULL){
                delete p_obj;
                p_obj = NULL;
                cout << "~Singleton" << endl;
            }
        }
};

Singleton* Singleton::p_obj = NULL;

void run(void){
    Singleton* s1 = Singleton::GetInstance();
    Singleton* s2 = Singleton::GetInstance();

    cout << "ADDR s1:" << s1 << endl;
    cout << "ADDR s2:" << s2 << endl;     
}

int main(){

    run();    

    cout << "Press enter to continue..." << endl;
    getchar();

    return 0;
}

在设计类的时候,声明了一个static Singleton类型的指针在类的私有成员区,需要注意的是,这个指针的内存单元并不会在对象中开辟,需要程序员自己在全局定义一个Singleton指针,如果不是单例模式的类,这种static类型的成员是每一个对象共享的,即所有的对象都可以访问在全局数据区的这一段static内存。

在类私有成员区还定义了构造函数,显然地,这样设计时不想让程序员可以直接创建Singleton对象,而是需要创建Singleton指针使用GetInstance方法去创建一个对象。这里需要我们知道C++编译器的一个特性:当程序员在设计类的时候,没有显示地声明构造函数,编译器会默认提供一个空的构造函数。这种懒汉式单例模式的实现,就是利用了C++的这一特性,将构造函数声明在类私有成员区,如果程序员在外部创建Singleton对象,编译器将报错:Singleton成员函数时私有的,不可访问。

如果想要调用Singleton类中的GetInstance函数创建对象,首先会判断一下p_obj指针是不是为空,即是不是还没有开辟过这个对象,如果确实目前还没有开辟过这个对象,那么使用new关键字在堆空间中开辟一段Sngleton大小的内存给申请者,而如果程序中早已存在Singleotn对象,则把已经存在的Singleton对象在堆空间中的地址返回给申请者。

其实这段程序是有漏洞的,在类中实现的析构函数并不会释放堆内存,因为它根本就没有被调用。这就造成了堆内存泄露的灾难。不能够在析构中释放内存,而是需要调用者手动地释放堆内存。改进这个漏洞后的代码如下:

#include <iostream>
#include <cstdio>

using std::cout;
using std::endl;

class Singleton {
    private :
        static Singleton* p_obj;
        Singleton(){

        }
    public :
        static Singleton* GetInstance(){
            if(p_obj == NULL){
                p_obj = new Singleton();
                cout << "New Singleton();" << endl;
                return p_obj;
            }
        }

};

Singleton* Singleton::p_obj = NULL;

void run(){
    Singleton* s1 = Singleton::GetInstance();
    Singleton* s2 = Singleton::GetInstance();

    cout << "ADDR s1:" << s1 << endl;
    cout << "ADDR s2:" << s2 << endl;    

    delete s1;
}

int main(){

    run();    

    cout << "Press enter to continue..." << endl;
    getchar();

    return 0;
}

显然,改进了的代码在单线程系统中不会再造成堆内存泄露。为什么要强调一下是单线程的系统中呢?因为它在多线程的系统中还是可能出问题,想象一下,同时两个线程运行到了if(p_obj == NULL),这时候会怎样?显然,会在堆内存中申请两个Singleton大小的内存段。然而我们只释放了一个,想象中他们是同一段,这就造成了泄露了一段Singleton大小的内存。改进这个漏洞的方法是在if(NULL != p_obj)前后加锁,改进如下:

#include <iostream>
#include <cstdio>

using std::cout;
using std::endl;

class Singleton {
    private :
        static Singleton* p_obj;
        Singleton(){            
        }
    public :
        static Singleton* GetInstance(){
            lock();
            if(p_obj == NULL){
                p_obj = new Singleton();
                cout << "New Singleton();" << endl;
                return p_obj;
            }
            unlock();
        }
};

Singleton* Singleton::p_obj = NULL;

void run(){
    Singleton* s1 = Singleton::GetInstance();
    Singleton* s2 = Singleton::GetInstance();

    cout << "ADDR P1:" << s1 << endl;
    cout << "ADDR P2:" << s2 << endl;    

    delete s1;
}

int main(){

    run();    

    cout << "Press enter to continue..." << endl;
    getchar();

    return 0;
}

然而这样做的在线程很多的时候会影响代码的效率,试想当多个线程都想要申请这么这段内存的时候,都被调度器阻塞,堆在一起等待,这是一件多可怕的事情。为了提高代码的运行效率,可以使用双重判断加锁,改进如下:

static Singleton* GetInstance(){
    if(p_obj == NULL){
        lock();
        if(p_obj == NULL){
            p_obj = new Singleton();
            cout << "New Singleton();" << endl;
            return p_obj;
        }
        unlock();
    }
}

多判断一次,减少多线程共同排队到这个位置的可能性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值