C++单例模式学习总结[可能存在错误结论,欢迎指正]

1 什么是单例模式

单例模式简单的来说就是:一个类只能有一个实例。

 

2 C++如何实现一个单例模式呢?

下面的代码就能够实现一个单例模式了~, 要点:

  • 定义构造函数为私有的
  • 定义一个私有的static的类对象指针
  • 定义给一个公有的static函数getInstance获取唯一一个实例化对象
class Singleton {
private:
    Singleton() {};
    static Singleton* m_pInstance;
public:
    static Singleton* getInstance() {
        if (m_pInstance == NULL) {
            m_pInstance = new Singleton();
        }
        return m_pInstance;
    }
};
// 静态成员变量在类中仅仅是声明,没有定义,所以要在类的外面定义,实际上是给静态成员变量分配内存
// 如果在类的外面有初始化值就用初始值初始化,否则系统就用默认值初始化它
Singleton* Singleton::m_pInstance = nullptr;

int main() {
    Singleton* instance = Singleton::getInstance();
    return 0;
}

上面的代码虽然能够实现需求,但是指针指向的内存却没办法释放,会导致内存泄露。我们使用VS调试工具测试一下:

使用VS进行内存检测调试:VS环境中进行内存泄漏的检测

引入头文件:#include <crtdbg.h>

在main函数最后加上_CrtDumpMemoryLeaks();

程序运行结束后查看调试结果会发现有内存泄漏警告!因此上面的单例模式代码是存在问题的,会造成内存泄露。

 

2.X 释放内存

经过认真学习我总结了如下几种解决方法:

方法X:错误警告!错误警告!错误警告!

这是错误的方法,能犯这种低级错误说明对于析构函数的认识不够到位!

一开始我想:直接在析构函数里面释放m_pInstance指针指向的空间不就好了吗?如果写出下面的代码那最终就会产生下图酷炫的结果:

#include "stdafx.h"
#include <iostream>
#include <crtdbg.h>
using namespace std;

class Singleton {
private:
    Singleton() {};
    static Singleton* m_pInstance;

public:
    ~Singleton() {
        printf("析构函数被调用");
        delete m_pInstance;
    }

    static Singleton* getInstance() {
        if (m_pInstance == NULL) {
            m_pInstance = new Singleton();
        }
        return m_pInstance;
    }
};
Singleton* Singleton::m_pInstance = nullptr;

int main() {
    Singleton* instance = Singleton::getInstance();
    delete instance;
    _CrtDumpMemoryLeaks();
    system("pause");
    return 0;
}

为什么会这样呢?因为析构函数只会在如下几种情况被调用 析构函数何时被调用

  1. 对象生命周期结束,被销毁时;
  2. 主动调用delete (和new配套使用);
  3. 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。

而在上面的代码中我们手动调用了delete,delete就会调用析构函数,然后析构函数里面又有一个delete,于是就会套娃,所以上面这种方法是错误的,还不如直接使用下面的方法1。

 

方法1:在代码中手动delete:

int main() {
    Singleton* instance = Singleton::getInstance();
    delete instance;
    return 0;
}

方法2:不使用new创建对象,使用局部静态变量

优点: 不需要考虑资源释放,程序结束时,静态区资源自动释放

class Singleton {
private:
    Singleton() {};
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
};

int main() {
    Singleton instance = Singleton::getInstance();
    system("pause");
    return 0;
}

方法3:进程结束时,静态对象的生命周期随之结束,其析构函数会被调用来释放对象。因此,我们可以利用这一特性,在单例类中声明一个内嵌类,该类的析构函数专门用来释放new出来的单例对象,并声明一个该类类型的static对象。

class Singleton {
private:
    Singleton() {};
    static Singleton* m_pInstance;
    class Garbo {
    public:
        ~Garbo() {
            if (Singleton::m_pInstance) {
                delete Singleton::m_pInstance;
            }
        }
    };
    static Garbo garbo;    // 定义一个静态成员,在程序结束时,系统会调用它的析构函数  
public:
    static Singleton* getInstance() {
        if (m_pInstance == NULL) {
            m_pInstance = new Singleton();
        }
        return m_pInstance;
    }
};
// 给静态变量分配内存
Singleton* Singleton::m_pInstance = nullptr;
Singleton::Garbo Singleton::garbo;

int main() {
    Singleton* instance = Singleton::getInstance();
    system("pause");
    return 0;
}

 

3 懒汉模式和饿汉模式

懒汉模式:故名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化。与之对应的是饿汉式单例(注意,懒汉本身是线程不安全的,为保证多线程下安全,必须加锁

饿汉模式:饿了肯定要饥不择食。所以在单例类定义的时候就进行实例化(本身就是线程安全的

懒汉模式[1]

第一次调用时才初始化,避免内存浪费,但是为保证多线程下安全,必须加锁(下面的代码没有解决资源释放问题)

#include "stdafx.h"
#include <iostream>
#include <mutex>
using namespace std;

class Singleton {
public:
    static Singleton* getInstance() {
        if (m_pInstance == nullptr) {
            lock_guard<mutex> lock(m_mutex);
            if (m_pInstance == nullptr) {
                m_pInstance = new Singleton();
            }
        }
        return m_pInstance;
    }
private:
    Singleton() {};
    static Singleton *m_pInstance;
    static mutex m_mutex;
};
Singleton* Singleton::m_pInstance = nullptr;
mutex Singleton::m_mutex;

int main() {
    Singleton::getInstance();
    system("pause");
    return 0;
}

 

饿汉模式

类加载时就初始化,会浪费内存,但线程安全

#include <iostream>
using namespace std;

class Singleton {
public:
    static Singleton* getInstance() {
        return m_pInstance;
    }
private:
    Singleton() {};
    static Singleton *m_pInstance;
};
// 直接初始化实例
Singleton* Singleton::m_pInstance = new Singleton();

int main() {
    Singleton::getInstance();
    delete Singleton::getInstance();
    return 0;
}

 

两种模式的特点与应用场景

  • 懒汉:在访问量较小时,采用懒汉实现。这是以时间换空间。
  • 饿汉:由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。

 

4 扩展(TODO)

针对单例模式可以扩展出如下问题:

问题2的答案:https://www.nowcoder.com/questionTerminal/0a584aa13f804f3ea72b442a065a7618

1. 将构造函数声明为私有的有哪些用途?

答:如设计一个单例模式

2. 将析构函数声明为私有的有哪些用途?

答:定义一个只能在堆上生成的对象,使用静态建立对象的方法(A a)会将对象放在栈中,由编译器自动释放空间,因此在建立对象的时候编译器会检查该类的构造函数和析构函数是否能正常访问。如果将析构函数定义为私有的,那么编译器就无法访问析构函数,因此会报错。所以如果将一个类的析构函数声明为私有的,该类只能在堆上生成对象。

原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

3. 类中的static成员函数和非static成员函数存在哪里?有什么区别? 

答:static成员函数和非static成员函数都存在代码区,但是非static函数有指向调用该函数对象的this指针,static函数没有this指针

4. 所有的类对象共享一份代码还是每个类都有一份代码?

答:所有类的对象共享一份代码,代码存在代码区。

 

References:

1. C++ 单例模式(懒汉、饿汉模式)

2. C++ 单例模式的实现及资源释放

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章