单例模式(Singleton Pattern)
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用。
特点:
- 程序运行中一个类始终只能创建一个对象
- 构造方法私有化
- 调用静态成员方法获取对象
使用场景:
Windows的Task Manager(任务管理器)、回收站:
无论打开几次任务管理器都只有一个程序。所有删除的文件都只丢进一个垃圾桶。没有多个实例的必要。应用程序的日志应用:
一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作。线程池:
使用线程池来管理线程的好处是线程池中的线程可以复用,在一个线程使用过时候,再返回到线程池中,不需要每次都创建一个线程。由于线程池是公共的,因此我们使用单例模式来保证线程池有且仅有一个。应用程序的日志应用:
如果不用单例的话,每次都要 new 对象,每次都要重新读一遍配置文件,很影响性能网站在线人数统计:
其实就是全局计数器,也就是说所有用户在相同的时刻获取到的在线人数数量都是一致的。要实现这个需求,计数器就要全局唯一,也就正好可以用单例模式来实现。当然这里不包括分布式场景,因为计数是存在内存中的,并且还要保证线程安全。
单例模式应用的场景一般存在以下条件:
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
- 控制资源的情况下,方便资源之间的互相通信。如线程池等。
实现方法分析:
- 由于构造方法是私有的所以在类外无法调用构造函数,故需在类中实现一个函数创建对象并且返回
- 由于成员函数需要由实例对象去调用,静态成员函数则可以用类名+类作用域符去调用。故该函数应该是静态成员函数。
- 由于没有拷贝构造函数。故函数返回值应该是对类的引用或者指针类型。
- 由于是用函数返回对象。防止在栈上初始化对象导致自动释放内存。故应该用指针(指针法:需要手动释放对象),或者用static(静态局部变量法:不需要手动释放对象)去创建对象。
- 由于该函数的每次调用必须保证返回值都是同一块内存,故如果用指针法需要将对象指针定义为类的静态成员,每次调用时判断是否为空再决定是否去分配内存。由于static只初始化一次,故不需要判断。
- 对象的释放。
指针方法:如果在析构函数中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();