单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用。

特点:

  • 程序运行中一个类始终只能创建一个对象
  • 构造方法私有化
  • 调用静态成员方法获取对象

使用场景:

  • Windows的Task Manager(任务管理器)、回收站:
    无论打开几次任务管理器都只有一个程序。所有删除的文件都只丢进一个垃圾桶。没有多个实例的必要。

  • 应用程序的日志应用:
    一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作。

  • 线程池:
    使用线程池来管理线程的好处是线程池中的线程可以复用,在一个线程使用过时候,再返回到线程池中,不需要每次都创建一个线程。由于线程池是公共的,因此我们使用单例模式来保证线程池有且仅有一个。

  • 应用程序的日志应用:
    如果不用单例的话,每次都要 new 对象,每次都要重新读一遍配置文件,很影响性能

  • 网站在线人数统计:
    其实就是全局计数器,也就是说所有用户在相同的时刻获取到的在线人数数量都是一致的。要实现这个需求,计数器就要全局唯一,也就正好可以用单例模式来实现。当然这里不包括分布式场景,因为计数是存在内存中的,并且还要保证线程安全。

单例模式应用的场景一般存在以下条件:

  • 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
  • 控制资源的情况下,方便资源之间的互相通信。如线程池等。

实现方法分析:

  1. 由于构造方法是私有的所以在类外无法调用构造函数,故需在类中实现一个函数创建对象并且返回
  2. 由于成员函数需要由实例对象去调用,静态成员函数则可以用类名+类作用域符去调用。故该函数应该是静态成员函数。
  3. 由于没有拷贝构造函数。故函数返回值应该是对类的引用或者指针类型。
  4. 由于是用函数返回对象。防止在栈上初始化对象导致自动释放内存。故应该用指针(指针法:需要手动释放对象),或者用static(静态局部变量法:不需要手动释放对象)去创建对象。
  5. 由于该函数的每次调用必须保证返回值都是同一块内存,故如果用指针法需要将对象指针定义为类的静态成员,每次调用时判断是否为空再决定是否去分配内存。由于static只初始化一次,故不需要判断。
  6. 对象的释放。
    指针方法:如果在析构函数中delete对象指针将再次调用析构函数 一直处于死循环。故我们需要提供相应的接口方法去释放。因为可能有多个指针变量指向这段内存,固需要用引用计数取记录指针的数量。当指向这段内存的指针数量为0时,释放这段内存。
    静态局部变量法:不需要手动释放变量

代码实现:

懒汉模式(四种方法)

#include<iostream>
using namespace std;


//返回指针+指针分配空间   --需要释放对象指针
class A
{
public:
    static A* GetA()
    {
        count++;
        if (m_pA == nullptr)
            m_pA = new A();
        return m_pA;
    }
    void DeleteA()   //释放对象指针
    {
        count--;
        if (count==0&&m_pA != nullptr)
        {
            delete m_pA;
            cout << "A对象指针被释放" << endl;
            m_pA = nullptr;
        }
    };
private:
    A(){};
    ~A(){};
    static A* m_pA; //返回的对象指针
    static int count;
};

A* A::m_pA = nullptr;
int A::count = 0; 



//返回指针+静态局部变量  --不需要释放对象指针
class B
{
public:
    static B* GetB()
    {
        static B m_B; 
        return &m_B;
    }

private:
    B(){};
};





//返回引用+静态局部变量    --不需要释放对象指针
class C
{
public:

    static C& GetC()
    {
        static C m_C; 
        return m_C;
    }

private:
    C(){};
};



//返回引用+指针分配空间  --需要释放对象指针
class D
{
public:
    static D& GetD()
    {
        count++;
        if (m_pD == nullptr)
            m_pD = new D();
        return *m_pD;
    }
    void DeleteD()   //释放对象指针
    {
        count--;
        if (count==0&&m_pD != nullptr)
        {
            delete m_pD;
            cout << "D对象指针被释放" << endl;
            m_pD = nullptr;
        }
    };
private:
    D(){};
    ~D(){};

    static D* m_pD; //返回的对象指针
    static int count;//引用计数

};

D* D::m_pD = nullptr;
int D::count = 0;

//主函数
int main()
{
    A* a1 = A::GetA();
    A* a2 = A::GetA();
    A* a3 = a1;


    cout <<"a1==a2:"<< (a1 == a2) << endl;
    cout << "释放a1" << endl;
    a1->DeleteA();
    cout << "释放a2" << endl;
    a2->DeleteA();

    B* b1 = B::GetB();
    B* b2 = B::GetB();
    cout <<"b1==b2:"<< (b1 == b2) << endl;

    C &c1 = C::GetC();
    C &c2 = C::GetC();
    cout <<"&c1==&c2"<< (&c1 == &c2) << endl;

    D &d1 = D::GetD();
    D &d2 = D::GetD();
    cout << (&d1 == &d2) << endl;
    cout << "释放d1" << endl;
    d1.DeleteD();
    cout << "释放d2" << endl;
    d2.DeleteD();

    cin.get();
    return 0;
}

最常用方法 :返回引用+静态局部变量
对GetA稍加修改,这个设计模板便可以适用于可变多实例情况,如一个类允许最多五个实例。**

存在的问题:

  • 在使用指针法情况下,在多线程情况下若多个线程同时初始化对象 则会返回多个内存破坏单例模式的性质

解决方法:

  • 进行加锁操作。在判断指针是否为空前加锁。分配后解锁
  • 引入饿汉模式,在程序运行前给静态指针分配空间

    懒汉模式

    //只需要更改初始化静态成员的代码即可
    A* A::m_pA = A::GetA();