1.什么是单例模式?
单例模式的的核⼼思想在创建类对象的时候保证⼀个类只有⼀个实例,并提供⼀个全局访问点来访问这个实例。
- 只有⼀个实例的意思是,在整个应⽤程序中,只存在该类的⼀个实例对象,⽽不是创建多个相同类型的对象。
- 全局访问点的意思是,为了让其他类能够获取到这个唯⼀实例,该类提供了⼀个全局访问点(通常是⼀个静态⽅法),通过这个⽅法就能获得实例。
2.单例设计模式的优点
- 全局控制:保证只有⼀个实例,这样就可以严格的控制客户怎样访问它以及何时访问它,简单的说就是对唯⼀实例的受控访问。
- 节省资源:也正是因为只有⼀个实例存在,就避免多次创建了相同的对象,从⽽节省了系统资源,⽽且多个模块还可以通过单例实例共享数据。
- 懒加载:单例模式可以实现懒加载,只有在需要时才进⾏实例化,能够提⾼程序的性能。
3. 单例设计模式的要求
- 私有的构造函数:C++类的私有权限使得构造函数只能在类内部被访问,防⽌外部代码直接创建类的实例。
- 私有的静态实例变量:C++中类的静态成员(变量或函数)归类所有,是类的所有实例所共享的。将实例变量设为静态,表明这个类的实例变量唯一。设为私有,辨明该实例在类外必须通过公有的接口来获得。
- 公有的静态方法getInstance():这个就是上面所述的获取唯一实例的借口。静态也使其具有唯一的性质。
4.单例模式的实现
按照实例创建的时机,单例模式有着多种实现方式,包括懒汉式、饿汉式等。
饿汉式:在类加载时就已经完成了实例的创建,不管后面创建的实例有没有使用,先创建再说,所以叫做 “饿汉”。
懒汉式:只有在请求实例时才会创建,如果在首次请求时还没有创建,就创建一个新的实例,如果已经创建,就返回已有的实例,意思就是需要使用了再创建,所以称为“懒汉”。
在多线程环境下,由于饿汉式在程序启动阶段就完成了实例的初始化,因此不存在多个线程同时尝试初始化实例的问题,但是懒汉式中多个线程同时访问 getInstance()
方法,并且在同一时刻检测到实例没有被创建,就可能会同时创建实例,从而导致多个实例被创建,这种情况下我们可以采用一些同步机制,例如使用互斥锁来确保在任何时刻只有一个线程能够执行实例的创建。
5.C++代码实战
小明的购物车https://kamacoder.com/problempage.php?pid=1074
题目描述:
小明去了一家大型商场,拿到了一个购物车,并开始购物。请你设计一个购物车管理器,记录商品添加到购物车的信息(商品名称和购买数量),并在购买结束后打印出商品清单。(在整个购物过程中,小明只有一个购物车实例存在)。
输入描述
输入包含若干行,每行包含两部分信息,分别是商品名称和购买数量。商品名称和购买数量之间用空格隔开。
输出描述
输出包含小明购物车中的所有商品及其购买数量。每行输出一种商品的信息,格式为 "商品名称 购买数量"。
输入示例
Apple 3
Banana 2
Orange 5
输出示例
Apple 3
Banana 2
Orange 5
提示信息
本道题目请使用单例设计模式:
使用私有静态变量来保存购物车实例。
使用私有构造函数防止外部直接实例化。
思路分析:
题目要求使用单例模式设计一个购物车管理器,那么这个管理器的类就是需要进行单例设置的类。包括私有的构造函数、私有的静态实例以及公有的实例获取方法接口。
代码实现:
#include<iostream>
#include<map>
#include<string>
using namespace std;
class ShoppingManager{
private:
//私有的构造函数防止类外实例化
ShoppingManager(){}
//使用map作为购物车存放商品名级商品数量
map<string,int> cart;
public:
//公有的静态接口函数,获取实例
static ShoppingManager& getInstance()
{
//静态实例。由于构造函数私有,所以该实例也算是必须类内调用构造函数才能得到。带有私有的含义
static ShoppingManager instance;
return instance;
}
//析构函数
~ShoppingManager(){}
//将商品添加到购物车。const保证数据不会误改,引用&避免拷贝
void addGoods(const string& name,const int& num)
{
cart[name] += num; //+=保证同名商品数量叠加而不是重写覆盖
}
//查看购物车. 注意const的作用
void viewCart() const
{
//正常访问
// for(auto it = cart.begin(); it!= cart.end();++it) {
// cout<<it->first<<" "<<it->second<<endl;
// }
//避免迭代器修改的访问。注意,迭代器类似指针,所以访问map的内容用->
// for(map<string,int>::const_iterator it = cart.cbegin(); it!=cart.cend();++it)
// {
// cout<<it->first<<" "<<it->second<<endl;
// }
//如果是范围for语句,则得到的是map的每一个成员,属于pair类型,用.点来访问内容
for( const auto & member: cart)
{
cout<<member.first<<" "<<member.second<<endl;
}
}
};
int main()
{
string name;
int num;
//按行读数据,可以使用while循环配合cin
while(cin>>name>>num)
{
//由于单例模式的构造函数私有,无法直接点用构造函数实例化,所以无法通过对象调用getInstance
//因此,可以使用类名::getInstance()的方式来获取单例
ShoppingManager & myCart = ShoppingManager::getInstance();
myCart.addGoods(name,num);
}
// 输出购物车内容。
const ShoppingManager& myCart = ShoppingManager::getInstance();
myCart.viewCart();
return 0;
}
由于实例是静态的,所以它的生存周期会在程序运行结束才销毁,且唯一,存放在全局区,因此即使第一次创建实例时是在while循环里面,第二次再使用getInstance获取它时,仍能获取到这个唯一的实例,其数据为最近一次更改后的数据。构造函数也旨在第一次构建实例时调用了,myCart.viewCart()前面的那句的getInstance代码并没有调用构造函数。读者可以在构造函数里打印一句话用以验证这一点。
6.应用场景
- 资源共享
多个模块共享某个资源的时候,可以使用单例模式,比如说应用程序需要一个全局的配置管理器来存储和管理配置信息、亦或是使用单例模式管理数据库连接池。
- 只有一个实例
当系统中某个类只需要一个实例来协调行为的时候,可以考虑使用单例模式, 比如说管理应用程序中的缓存,确保只有一个缓存实例,避免重复的缓存创建和管理,或者使用单例模式来创建和管理线程池。
- 懒加载
如果对象创建本身就比较消耗资源,而且可能在整个程序中都不一定会使用,可以使用单例模式实现懒加载