单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类仅有一个实例,并提供一个全局 访问点来获取该实例。这种模式在很多情况下都非常有用,尤其是在那些需要一个对象来协调动作并确 保对象一致性的场景中。
1 实现思路
确保用户无法自己创建对象(构造函数私有化,删除拷贝构造函数)
让类自己负责管理这唯一的一个对象(使用静态成员变量)
普通的成员属于对象,而静态成员变量不属于对象。
不能在构造函数中定义和初始化,需要在类的外部单独定义和初始化。
静态成员变量和全局变量一样存放在全局区,可以把静态成员变量理解成是被限制在类中去使
用的全局变量。
4.提供一个安全的全局访问点,即提供一个公开的接口,让用户可以使用这唯一的一个实例(静态成员 函数)
静态成员函数没有this指针,没有const属性,但是可以重载(静态与非静态成员函数名字相同
且参数相同不构成重载)
访问方式:
类名::静态成员函数(实参表);//推荐
对象名.静态成员函数(实参表);//和上面方法本质一样
在静态成员函数中只能访问静态成员,不能访问普通成员,在普通的成员函数中,既可以访问静态
的成员也可以访问普通的成员。
静态成员变量的类型是一个类类型(如自定义的类),那么它就是一个对象
如果它的类型是基本数据类型(如
int
、float
等),那么它就不是对象,而是一个变量
不同的语言,不同的程序员,可能有多种实现方式,总体上可以分为两种:饿汉式和懒汉式
1.1 饿汉式
饿汉式单例模式(预先创建):(无论用或不用,程序启动即创建)
- 唯一的实例在类加载的时候创建,预先初始化(不管有没有用到,都创建这个实例)
- 在类加载的时候创建,调用的时候反应速度快
- 在类加载的时候创建,有可能永远也用不到这个实例(资源浪费)
静态成员变量是在类加载时就创建并初始化的,因此在程序开始前就已经创建了单例对象
#include <iostream>
using namespace std;
//单例模式:饿汉式
class Singlteton
{
public:
//3)通过静态成员函数获取单例对象
static Singlteton &getInstance()//非局部变量 不加&会报错,因为不加&会多调用一次拷贝构造
// 但是拷贝构造已经删除
{
return s_instance;
}
void print()
{
cout << m_data <<endl;
}
// 1。构造函数私有化,删除拷贝构造
private:
Singlteton(int data = 0):m_data(data)
{
cout << "创建单例对象" << endl;
}
// detele 表示删除该函数
// 只有一个对象,不考虑有两个对象
Singlteton(const Singlteton &) = delete;
// (2)通过静态成员变量维护唯一的对象
//静态成员变量是在类加载时就创建并初始化的,因此在程序开始前就已经创建了单例对象(在全局区)
static Singlteton s_instance;//不属于类,不分配内存
private:
int m_data;
};
// 1.Singlteton 该类型的变量 2.Singlteton表示 s_instance是类里面的成员
Singlteton Singlteton:: s_instance(12345);
// class A
// {
// string str;
// // A a; error 为什么?因为类型没有定义完整,没办法分配内存大小
// }
int main()
{
cout << "main开始执行" << endl;
// 如果不定义为引用,就会调用拷贝构造函数生成新对象,但是构造函数被私有化了
// Singlteton s1 = Singlteton::getInstance();
Singlteton &s1 = Singlteton::getInstance();//使用引用时,你实际上是在操作原始对象,而不是创建一个新对象的拷贝
Singlteton &s2 = Singlteton::getInstance();
cout << "&s1 = " << &s1 << endl;
cout << "&s2 = " << &s2 << endl;
s1.print();
s2.print();
return 0;
}
- 注意:
- 问题1:为什么
class Singleton
可以包含static Singleton s_instance
静态成员?
- 问题1:为什么
静态成员的生命周期:静态成员变量与类本身相关联,而不是与类的任何特定对象相关联(静态成员不
属于类)。静态成员变量的生命周期是整个程序的执行期间。这意味着静态成员变量在程序开始运行时
被创建,并在程序结束时被销毁。这个特性使得静态成员变量非常适合用于实现单例模式,因为单例模
式要求有一个全局唯一的对象实例,该实例在程序的生命周期内一直存在。
**初始化时机:**在类定义外部,我们可以对静态成员变量进行初始化。这通常是在类定义之后的某个地
方,或者在包含类定义的源文件的顶部。这个初始化只执行一次,且发生在程序启动时。因此,对于饿
汉式单例模式,我们在类外部初始化静态成员变量s_instance
,这样当程序开始运行时,单例对象就
已经被创建并准备好了。
**访问权限:**静态成员变量可以通过类名直接访问,而不需要创建类的对象。这使得静态成员变量成为实
现单例模式的理想选择,因为单例模式的设计目标就是允许全局访问唯一的对象实例,而不需要(也不
允许)创建该类的多个实例。
问题2:
class Singleton
可以包含Singleton s_instance
非静态成员吗?
不可以,因为class Singleton
还未创建出来,不确定其大小,但是可以包含Singleton *s_instance 类型指针变量,因为指针的大小是确定的。类的定义必须是完整的,编译器才能确定类的大小。
问题3:为什么在Singleton &s1 = Singleton::getInstance();
要在s1 前加&
因为加&表示s1是这个单例对象的引用,不产生新的对象;而不加&表示创建一个新的s1对象,用这个单例对象来初始化,但是构造函数被私有化了,类外无法访问,拷贝构造函数被禁用了。
问题4:饿汉式单例模式中,单例对象具体是在什么时候创建的?
在饿汉式单例模式中,单例对象是在程序启动时创建的,具体来说,是在全局静态变量的初始化阶段。
这个初始化过程发生在程序的静态存储区分配和初始化阶段,通常是在main 函数执行之前(新词将该
单例初始化代码放在main函数后也会比main函数先执行)。
对于饿汉式单例模式,当你定义了一个静态成员变量并给它一个初始值,如 static Singleton
s_instance(12345);
,这个变量会在程序启动时自动被初始化。这个初始化过程是由编译器在编译时
确定的,不需要任何额外的代码或函数调用。
下面是一个更详细的解释:
当编译器遇到类定义中的静态成员变量时,它会记住这个变量需要在程序启动时初始化。
在链接阶段,编译器和链接器会合作确定所有全局和静态变量的初始化顺序。对于静态成员变量,它们
通常按照它们在代码中的声明顺序进行初始化。
当程序开始执行时,在main 函数之前,全局对象和静态对象(包括静态成员变量)的构造函数会被调
用,进行初始化。
对于饿汉式单例模式中的 static Singleton s_instance(12345);
,当程序开始执行时,
s_instance 的构造函数会被调用,使用提供的参数 12345 进行初始化。
一旦s_instance
被初始化,它就存在于程序的整个生命周期中,可以通过
Singleton::getInstance()
方法安全地访问。
由于饿汉式单例模式的初始化是在程序启动时完成的意味着即使单例对象在程序的某些部分中并未使
用,它仍然会占用内存空间。
相比之下,懒汉式单例模式将对象的创建推迟到第一次需要它的时候,这可以节省内存,但在多线程环
境下需要额外的同步机制来确保线程安全。
1.2 懒汉式
- 懒汉式单例模式(延时创建);(用时再创建,不用即销毁)
- 唯一的实例在第一次调用获取实例接口的时候创建,延时初始化
- 不用到就永远不会创建
- 避免了饿汉式那种在没有用到的情况下也会创建实例的问题,资源利用率高
- 在使用的时候创建,第一次调用的时候反应速度没有饿汉式快
#include <iostream>
using namespace std;
class Singlteton
{
// 3.通过静态成员函数获取单例对象 公开接口
public:
static Singlteton &getInstance()
{
if(s_instance == nullptr)
{
s_instance = new Singlteton(54321);
}
s_count++;
return *s_instance ;
}
// 单例模式可以被多人使用,最后一个使用者负责回收
void release()
{
//减少计数
if(s_count > 0)
{
s_count--;
}
if(s_count == 0)
{
delete s_instance ;
s_instance = nullptr;
}
}
void print()
{
cout << m_data << endl;
}
~Singlteton()
{
cout << "单例对象被销毁了" << endl;
}
public:
Singlteton(const Singlteton &) = delete;
// 2通过静态成员变量维护唯一的对象
// 虽然s_instance还是在全局区,但是初始化为空了,对象还没有创建
static Singlteton *s_instance;//指针可以指向空
// 计数,用于记录单例对象的人数
static int s_count;//静态成员变量
private:
//1)私有化构造函数
Singlteton(int data = 0): m_data(data)
{
cout << "创建单例对象" << endl;
}
int m_data;
};
Singlteton *Singlteton::s_instance = NULL;//s_instance 是一个指向 Singlteton 类型的指针
// Singlteton Singlteton::*s_instance 表示 s_instance 是一个指向 Singlteton 类型的成员变量的指针,而不是一个指向 Singlteton 类型的指针
int Singlteton::s_count = 0;
int main()
{
cout << "main开始执行" << endl;
Singlteton &s1 = Singlteton::getInstance();
Singlteton &s2 = Singlteton::getInstance();
cout << "&s1 = " << &s1 << endl;
cout << "&s2 = " << &s2 << endl;
s1.print();
s1.release();
s2.print();
s2.release();
return 0;
}
问题:
问题1:为什么这个实例中单例对象要定义为
static Singleton s_instance;
而不是static Singleton* s_instance;
在懒汉式单例模式的实现中,单例对象通常定义为指向单例类型的静态指针(s_instance; ),而不是静态对象(static Singleton*
- static Singleton s_instance; ),主要有以下几个原因:
- 延迟初始化:懒汉式单例模式的核心思想是将单例对象的创建延迟到第一次真正需要它的时候。如果使
- 用静态对象,那么单例对象会在程序启动时就被创建,这违背了懒汉式单例的初衷。而使用静态指针,
- 我们可以控制单例对象的创建时机,确保在第一次调用
- getInstance 方法时才进行创建。
- 动态内存分配:使用指针允许我们动态地分配内存给单例对象。这意味着我们可以利用
- new 操作符在堆
- 上创建单例对象,并通过指针来管理它。这种方式提供了更大的灵活性,特别是在处理大型对象或需要
- 更多控制对象生命周期的情况下。
- 易于销毁:通过指针,我们可以更容易地销毁单例对象。当不再需要单例对象时,我们可以使用
- delete 操作符释放其占用的内存。如果使用静态对象,则对象的生命周期将与程序的执行期相同,无
- 法手动销毁。虽然对于单例模式来说,通常不需要显式销毁单例对象,但在某些特殊情况下(如单元测
- 试或资源清理),手动销毁可能是有益的。