【设计模式】——单例模式

本文详细介绍了设计模式中的单例模式,包括单例模式的概念、设计思想、实现方式、特点以及应对多线程的安全策略。通过示例展示了如何在C++中创建单例,探讨了懒汉模式和饿汉模式的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、设计模式

1.1对设计模式的理解

关于什么是设计模式这个问题,官方的解释是设计模式是一套被反复使用的,多数人直知晓的、经过分类编目的、代码设计经验的总结。
其实通俗一点来理解,每一个设计模式描述了一个在我们周围不断重复发生同种相似问题,以及该问题的解决方案核心。有了设计模式过后就可以一次又一次地使用该方案而没有必要做重复劳动了

1.2设计模式的分类

总共有23种设计模式,这些模式可以分为创建型模式、结构型模式、行为型模式三大类。

  1. 创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。
  2. 结构型模式:把类或对象结合在一起形成一个更大的结构。
  3. 行为型模式:类和对象如何交互,及划分责任和算法
    主要分类如下图所示:
    在这里插入图片描述

2、单例模式

Singleton模式是设计模式中最为简单、最为常见、最容易实现的模式。他主要解决的是怎样去创建一个唯一的对象(变量)。

2.1设计思想


因为单例模式主要是解决怎样去创建一个唯一对象的问题。所以我们首先要讨论一下在c++中我们是如何来生成对象的。
一般对象的生成分为两步:

  1. 开辟对象所占的内存空间
  2. 调用构造函数

假如想通过控制第一步来实现

由于第一步为系统调用,人为不能控制对象的生成,也就没办法来决定对象的生成个数,所以不可行

假如想通过控制第二步来实现
虽然构造函数还是系统调用,但是可以自己编写,并且可以控制其访问限定符。那么接下来我们就来思考应该把自己实现的构造函数放在哪个访问限定符下呢?

  • 如果放在public访问限定符之下:public代表成员在任意位置(类中、类外、全局、局部等等)可以访问,说明可以在任意的位置调用构造函数生成对象。这样也没办法来控制对象的生成个数。所以不可行
  • 如果放在private访问限定符之下:,私有的只能在本类类中访问。类外就不能调用构造函数就没有办法来生成对象了。这样就实现了对象生成接口的屏蔽

2.2设计一个单例模式

有了上面的设计思想。我们就可以来简单的设计一个单例模式了。
【举个栗子】
一个学校只有一个校长,其中有三个成员变量包括姓名、性别、和年龄。

第一步:屏蔽对象生成的接口
主要通过将构造函数设计在private下来控制对象的生成只能在本类类中,具体实现如下:

class Rector
{
    public:
    private:
        Rector(char* name,int age,bool sex)
        {
            mname = new char[strlen(name)+1]();
            strcpy_s(mname,strlen(name)+1,name);
            mage = age;
            msex = sex;
        }
        char* mname;
        int mage;
        bool msex;
};

第二步:设计一个接口,用于生成这个唯一的对象,还要把这个对象返回出去在类外使用

【思考1】返回值应该设置成何种方式?
一般的,我们的返回值类型有三种方式,第一种本身类类型,第二种类类型的引用,第三种类类型的指针到底用哪种比较合适呢?
因为整个设计模式是让类生成一个对象,如果用类类型方式返回是有临时对象产生的所以不可行,后面两种方式都可以因为没有临时对象的产生都可以使用。

【思考2】整个函数的调用能不能依赖对象调用,如何调用?
不能依赖对象调用。接口的核心是生成唯一对象的,如果依赖对象调用第一个对象永远没有办法生成。变成了相互依赖。所以,我们要把这个接口写成静态的成员函数,因为静态成员方法是不依赖对象调用的

有了上面两个两个思考的解决方案过后,我们就可以具体的实现这个函数接口了,具体实现如下:

static Rector* getInstance(char*name,int age,bool sex)
{
    if(pre == NULL)
    {
        pre = new Rector(name,age,sex);
    }
    return pre;
}

在实现的过程中我们还应该有一个标识来标识唯一的对象,它不属于对象是属于整个类作用域的,所以设置为静态的成员变量,然后在类外对其进行初始化。

总结:单列模式设计点

  1. 屏蔽生成对象的接口:构造函数,拷贝构造函数访问限定符为私有private
  2. 通过接口来生成唯一的对象:不能以类类型返回,用static关键字修饰。
  3. 定义指针指向唯一的对象,用来标识唯一的对象。

基于上述设计点,整个单例模式实现如下:

class Rector
{
public:
	static Rector* getInstance(const char* name, int age, bool sex)
	{
		if (pre == NULL)
		{
			pre = new Rector(name, age, sex);
		}
		return pre;
	}
	static void show()
	{
		std::cout << "----唯一的校长信息---" << std::endl;
		std::cout << "name: " << pre->mname << std::endl;
		std::cout << "age: " << pre->mage << std::endl;
		std::cout << "sex: " << pre->msex << std::endl;
		std::cout << "---------------------" << std::endl;
	}
private:
	Rector(const char* name, int age, bool sex)
	{
		mname = new char[strlen(name) + 1]();
		strcpy_s(mname, strlen(name) + 1, name);
		mage = age;
		msex = sex;
	}
	char* mname;
	int mage;
	bool msex;
	static Rector* pre;
};
Rector* Rector::pre = NULL;

int main()
{
	Rector* pr1 = Rector::getInstance("zhangsan", 45, true);
	Rector* pr2 = Rector::getInstance("zhangsan", 45, true);
	Rector* pr3 = Rector::getInstance("zhangsan", 45, true);
    Rector::show();
	std::cout << "pr1" << pr1 << std::endl;
	std::cout << "pr2" << pr2 << std::endl; 
	std::cout << "pr3" << pr3 << std::endl;
	return 0;
}

运行结果如下:
在这里插入图片描述

在使用的时候是通过结构来生成对象,并且在调试的过程中会发现,这三个指针其实都指向的同一个对象。说明我们生成了唯一的一个对象,实现了单例模式。

2.3单例模式的模型

因为在上一个设计的单例模式中我们只是单方面的只考虑了构造函数是用来生成对象的,殊不知,其实类中的拷贝构造函数也是可以用来生成对象的。

所以针对拷贝构造函数也应该处理。所以将拷贝构造函数的申明还是写在私有下
【注意】不用写拷贝构造函数的实现,因为如果一旦拷贝构造函数能够调用成功就证明有两个对象生成,这就不符合要求了。
解决了这个问题,单例模式的模型实现如下:

class SingleTon
{
    public:
    static SingleTon* getInstance()
    {
        if(psing == NULL)
        {
            psing = new SingleTon();
        }
        return psing;
    }
    private:
        SingleTon()//私有构造
        {
        }
        SingleTon(const SingleTon&);//私有拷贝构造
        static SingleTon* psing;
};
SingleTon* SingleTon::psing = NULL;
int main()
{
    SingleTon * pr1 = SingleTon::getInstance();
    SingleTon * pr2 = SingleTon::getInstance();
    SingleTon * pr3 = SingleTon::getInstance();
    return 0;
}

2.4单例模式的特点及改进策略

1、特点
【优点】

  • 在内存中只有一个对象,节省内存空间
  • 避免频繁的创建销毁对象,可以提高性能
  • 避免对共享资源的多重占用。

【缺点】
我们定义的标识唯一一个对象的静态指针是一个临界资源,对这块临界资源的操作不是原子操作,在任意时刻都可以被中断,那么如果临界资源被多个线程访问修改,那么就会出现线程不安全。如

  • A线程得到了这临界资源进行了if空判断,判断完成刚准备生成对象时。
  • B线程抢占了临界资源,此时临界资源还是空,表示还没有指向唯一的对象,然后B对其进行对象的生成和指向,此时临界资源标识了唯一的对象。
  • 此时A线程继续,继续动态开辟,又重新生成了对象,临界资源指向新生成的对象。

2、改进策略
所以我们采用了锁的方式来解决线程安全问题。

【注意!】效率不高的加锁方式
因为我们都知道,对于线程安全问题,采用锁的方式最容易解决。当A线程在创建这个对象的之前先加锁,在A线程创建对象的过程中,虽然时间片到了,轮到B线程来访问函数,但是由于函数是锁着的,就一直阻塞。当A线程创建对象完毕过后再解锁,如下图所示:
在这里插入图片描述
但是这样,每次调用都需要进行解锁,加锁,即使唯一的对象已经生成了。从逻辑上来说,当第一个对象已经生成时,后面的调用不需要对临界资源进行完整操作,只需要判断即可。现在每次都要加锁解锁,浪费系统资源,效率低

1、解决方法1——双重锁机制下的单例模式

使用 双重锁机制处理,双重if,这样,第一个if直接排除了第二次以及后来调用接口的可能,不会进入if判断,就不会进行加锁解锁操作。

  • 当lazy!=NULL, 表示是第二次或后来的调用,不进入if。
  • 当lazy==NULL,表示现在唯一的对象还没有生成,进入if,进行加锁,解锁。

具体实现如下:

pthread_mutex_t mutex;
class SingleTon
{
    public:
    static SingleTon* getInstance()
    {
        if(pre == NULL)
        {
            lock();
            if(pre == NULL)
            {
            	pre = new SingleTon();
            }
            unlock();
        }
        return psing;
    }
    private:
        SingleTon()//私有构造
        {
        }
        SingleTon(const SingleTon&);//私有拷贝构造
        static SingleTon* psing;
};
SingleTon* SingleTon::psing = NULL;

2、解决方法2——在线程开启之前生成对象

还有另外一种思考角度来解决线程安全问题,我们可以思考一下线程是是什么?——他其实就是进程中的一个执行序列,也就是说有了进程过后才会有线程这一说法,而进程的入口值mian函数。
所以为了避免线程安全问题的出现,干脆在还没有线程的时候就把这个唯一的对象给生成了,也就是说在线程开启之前(main函数执行之前就把这个唯一的对象生成)具体实现如下:

class SingleTon
{
    public:
    static SingleTon* getInstance()//唯一的共有接口,返回已经生成好的唯一的对象
    {
        return psing;//返回唯一的对象
    }
    private:
        SingleTon()
        {
        }
        SingleTon(const SingleTon&);
        static SingleTon* psing;
};
SingleTon* SingleTon::psing = new SingleTon();
int main()
{
}

运行结果如下:
在这里插入图片描述
【举个栗子】在饿汉模式下实现唯一的一个校长对象

class Rector
{
public:
	static Rector* getInstance()
	{
		return reator;
	}
	static void show()
	{
		std::cout << "----唯一的校长信息---" << std::endl;
		std::cout << "name: " << reator->mname << std::endl;
		std::cout << "age: " << reator->mage << std::endl;
		std::cout << "sex: " << reator->msex << std::endl;
		std::cout << "---------------------" << std::endl;
	}
private:
	Rector(const  char* name, int age, bool sex)//构造函数
	{
		mname = new char[strlen(name) + 1]();
		strcpy_s(mname, strlen(name) + 1, name);
		mage = age;
		msex = sex;
		std::cout << "Rector finsh" << std::endl;
	}
	Rector(const Rector&);//拷贝构造函数
    char* mname;
	int mage;
	bool msex;
	static Rector* reator;//指向唯一的对象
};
Rector* Rector::reator = new Rector("wxy",21,false);//饿汉模式,提前生成唯一的对象

int main()
{
	Rector* pr1 = Rector::getInstance();
	Rector* pr2 = Rector::getInstance();
	Rector* pr3 = Rector::getInstance();
	Rector::show();
	std::cout << "pr1" << pr1 << std::endl;
	std::cout << "pr2" << pr2 << std::endl; 
	std::cout << "pr3" << pr3 << std::endl;
}

运行结果如下:
在这里插入图片描述

2.5懒汉模式和饿汉模式

其实上面的两种解决方案也就对应了我们的两个模式,分别是懒汉模式和饿汉模式。

1、懒汉模式
懒汉模式是延时加载,这个唯一的对象是在使用的时候才生成。我们的普通方式还有双重锁机制就是如此。他主要有如下的特点。
【优点】

  • 资源利用率高;只有调用该类的公有接口时才会创建唯一的对象。
  • 加载类速度快。

【缺点】

  • 存在线程安全问题,需要引入同步机制;如果程序在多线程情况运行,创建唯一对象的操作不是原子操作,所以就存在临界资源被多个线程访问抢占的情况出现,导致线程不安全问题,所以需要引入同步机制,如互斥锁,信号量等来控制唯一对象的生成。
  • 运行获取速度慢, 多线程同步机制带来额外开销。

2、饿汉模式
饿汉模式是贪婪加载,这个唯一的对象是提前生成的。
【优点】

  • 线程安全:在产生线程之前就完成唯一对象的创建,所以不会存在线程不安全问题。
  • 运行时获取对象速度快:因为在类加载时已经创建,运行只用获取即可。

【缺点】

  • 存在资源浪费的可能;如果不需要生成唯一的对象,只是加载了该类,还是会创建一个对象,资源效率不高。
  • 类加载时速度慢;需要创建对象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值