C++单例模式总结

单例模式的实现步骤

实现步骤

1.首先你得有个类

2.将构造、拷贝构造、赋值运算符私有化(如果用不到可以将拷贝构造和赋值运算符只声明不定义)

3.提供唯一的类实例对象。

4.对外提供获取实例唯一接口。

实现样例

 browser.h

class Browser
{

public:
    static Browser *getInstance();
    ~Browser();
private:
    Browser();
    Browser(const Browser &);
    Browser &operator=(const Browser &);

    static Browser *m_browser;
};

browser.cpp

Browser *Browser::m_browser = new Browser;

Browser *Browser::getInstance()
{
    return m_browser;
}

"唯一实例"的几种实现方式

上述例子中通过全局静态指针,在类外初始化唯一实例。除此之外还有其他几种方式:

全局静态指针,在类内初始化

 browser.h

class Browser
{

public:
    static Browser *getInstance();
    ~Browser();
private:
    Browser();
    Browser(const Browser &);
    Browser &operator=(const Browser &);

    static Browser *m_browser;
};

browser.cpp

Browser *Browser::m_browser = nullptr;

Browser *Browser::getInstance()
{
    if(m_browser == nullptr)
        m_browser = new Browser();
    return m_browser;
}

全局静态变量

 browser.h

class Browser
{

public:
    static Browser *getInstance();
    ~Browser();
private:
    Browser();
    Browser(const Browser &);
    Browser &operator=(const Browser &);

    static Browser m_browser;
};

browser.cpp

Browser *Browser::getInstance()
{
    return &m_browser;
}

局部静态指针

 browser.h

class Browser
{

public:
    static Browser *getInstance();
    ~Browser();
private:
    Browser();
    Browser(const Browser &);
    Browser &operator=(const Browser &);
};

browser.cpp

Browser *Browser::getInstance()
{
    Browser *browser = new Browser();
    return browser;
}

局部静态变量

 browser.h

class Browser
{

public:
    static Browser *getInstance();
    ~Browser();
private:
    Browser();
    Browser(const Browser &);
    Browser &operator=(const Browser &);
};

browser.cpp

Browser *Browser::getInstance()
{
    Browser browser;
    return &browser;
}

局部静态引用

 browser.h

class Browser
{

public:
    static Browser &getInstance();
    ~Browser();
private:
    Browser();
    Browser(const Browser &);
    Browser &operator=(const Browser &);
};

browser.cpp

Browser &Browser::getInstance()
{
    Brower browser;
    return browser;
}

上述几种方式中有采用指针的,有采用变量的也有采用引用的。有关线程安全、效率的问题后面会提到。这里先谈一个需求:如果想要在不结束程序的情况下,释放单例对象。(暴捶产品~)

这种情况下需要主义的几点:

  • 首先避免在单例的析构中释放对象。因为会无线循环下去。。。
  • 其次,对于局部静态变量和全局静态变量,是采用取地址方式返回指针,所以不可以在外部直接释放这个指针(因为delete的不是new分配的),
  • 而静态引用,无法释放。
  • 对于局部静态指针,可能想从外部获取到指针,然后释放。但是问题来了,内存确实可以释放,但是指针对象无法处理(因为外部指针对象和内部指针对象虽然指向同一个内存,但是不是一个指针,所以即使在外部释放这个内存,并将指针指向nullptr,也不会改变单例中的那个指针),所以在下次调用getInstance时,browser非空,被返回,导致指针指向以释放的内存。

一系列排除后只剩下全局指针了。同样还是不能够在析构中释放,我们需要提供一个释放的接口,由调用者主动释放。

void Browser::destroyInstance()
{
	if (m_browser) {
		delete m_browser;
		m_browser = nullptr;
	}
}

懒汉or饿汉

顾名思义,懒汉式表示很懒,用的时候初始化,不用不初始化;饿汉式表示很饿,很急,所以上来就初始化。上述几种唯一实例中除了全局静态指针,类外初始化和全局静态变量为饿汉式,其他均为懒汉式。

区别

初始化时间:对于比较费时的单例对象,饿汉式会导致程序启动慢,但是后续运行不会卡顿,而懒汉式则启动快,后续初始化单例时会费时。

内存:饿汉式不论是否使用都会初始化,占用内存,而懒汉式不用不初始化。

线程安全问题:饿汉式因为初始化都在main函数之前,此时线程只有一个,不存在线程竞速问题。而懒汉式则存在线程竞速的问题。

non-local static的初始化顺序问题:由于C++对于non-local static变量的初始化顺序并不保证,所以当实例A调用实例B,而实例B初始化在A之后,则会出现问题。

主动释放:采用全局静态指针,两者都可以主动释放。

懒汉式的线程安全

前面提到懒汉式是非线程安全的。具体原因是

Browser *Browser::getInstance()
{
    if(m_browser == nullptr)
        m_browser = new Browser();
    return m_browser;
}

 对于上面全局静态指针的懒汉式,在线程A运行到

m_browser = new Browser();

 时(未执行),跳转到线程B并执行了

    if(m_browser == nullptr)

 这样就会导致多份m_browser的情况了。

检查锁

QMutex mutex;

Browser *Browser::getInstance()
{
    mutex.lock();
    if(m_browser == nullptr){
        m_browser = new Browser();
    }
    mutex.unlock();
    return m_browser;
}

 采用单锁的方式,确保同一时间只能有一个线程能够进入判断。但是每次getInstance都需要加锁,非常费时。

双检查锁

QMutex mutex;

Browser *Browser::getInstance()
{
    if(m_browser == nullptr){
        mutex.lock();
        if(m_browser == nullptr){
            m_browser = new Browser();
        }
        mutex.unlock();
    }
    return m_browser;
}

 通过上述两层检查可以解决单锁的效率问题。但是。。。还有问题

双检查锁的问题——非原子操作

直到

m_browser = new Browser();

 之前,一切都是美好的,但是因为这一句并非原子操作,导致双检查锁并不安全。这条语句可以分为

  • new分配内存
  • 调用Browser()构造初始化内存
  • 将m_browser指向初始化后的内存

由于第二句依赖于第一句,所以两者顺序并不会改变,但是第二句和第三句可能由于编译器优化,导致顺序颠倒,这样线程A在此处将m_browser指向一片内存(未初始化),而线程B来到getInstance中,判断m_browser非空,很开心返回这个指针到外部使用,然后崩了。。。

volatile关键字

为了避免编译器优化操作顺序,C++11提供了volatile关键字,通过volatile修饰的关键字结合双检查锁可以实现线程安全的懒汉式单例。

但是其使用实在不够友好:

browser.h

class Browser
{
	Q_OBJECT
public:
	static volatile Browser *getInstance();
	static void destroyInstance();
	~Browser();
private:
	Browser();
	Browser(const Browser &);
	Browser &operator=(const Browser &);

	static Browser *volatile m_browser;
	static QMutex m_mutex;
};

 browser.cpp

Browser *volatile Browser::m_browser = nullptr;
QMutex Browser::m_mutex;
volatile Browser *Browser::getInstance()
{
	if (m_browser == nullptr) {
		QMutexLocker lock(&m_mutex);
		if (m_browser == nullptr) {
			m_browser = new Browser();
		}
	}
	return m_browser;
}

内存栅栏

内存栅栏确保命令执行的相对顺序,却不决定执行时序,其之后的内存访问操作一定在其之前的内存访问操作之后执行。所以我峨嵋你

Browser *volatile Browser::m_browser = nullptr;
QMutex Browser::m_mutex;
volatile Browser *Browser::getInstance()
{
	if (m_browser == nullptr) {
		QMutexLocker lock(&m_mutex);
		if (m_browser == nullptr) {
            auto tmp = new Browser();
            //内存栅栏操作,确保下面赋值时tmp肯定初始化好了
			m_browser = tmp;
		}
	}
	return m_browser;
}

 具体的可以通过atomic_thread_fence和std::atomic实现。

call_once

这是C++11引入的比较简单的方式,需要用到一个全局标志位和call_once函数。

std::once_flag oc;
Browser *Browser::getInstance()
{
	std::call_once(oc, [&]() {m_browser = new Browser; });
}

其原理为:全局标志oc如果为false,则进入call_once进行初始化,否则不允许进入。

其他

由于C++11确保局部静态变量的初始化是线程安全的,所以我们还可以采用Scoffmeyer式,也就是前面所说的局部静态引用方式。

 局部静态引用

 browser.h

class Browser
{

public:
    static Browser &getInstance();
    ~Browser();
private:
    Browser();
    Browser(const Browser &);
    Browser &operator=(const Browser &);
};

browser.cpp

Browser &Browser::getInstance()
{
    Brower browser;
    return browser;
}

总结

1.如果没有对内存的主动释放要求,可以采用Scoffmeyer式或call_once,比较简单

2.如果有要求则采用全局静态指针的懒汉式,并采用call_once初始化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值