目录
一.专栏简介
本专栏是我学习《head first》设计模式的笔记。这本书中是用Java语言为基础的,我将用C++语言重写一遍,并且详细讲述其中的设计模式,涉及是什么,为什么,怎么做,自己的心得等等。希望阅读者在读完我的这个专题后,也能在开发中灵活且正确的使用,或者在面对面试官时,能够自信地说自己熟悉常用设计模式。
本章将开始单件模式的学习,也叫单例模式。
二.引出单件模式
单件模式编写的类只能实例化出仅"一个"对象,而且是唯一的对象。单件模式给我们一个全局的访问点,就像全局变量一样,但没有全局变量的缺点。如果把一个对象赋值给一个全局变量,那么这个对象可能在应用启动时就被创建。要是这个对象是资源密集型的,创建之后还从来没有被使用过,那就是一个比较大的浪费了。单件模式允许我们在需要的时候再创建这个单件对象。
我们怎么令一个类不能实例化?令这个类的构造函数私有。又怎么调用构造函数实例化一个对象呢?用这个类里面的静态函数调用这个构造函数。代码如下:
Singleton.h:
cpp
#pragma once
class MyClass
{
public:
static MyClass* getInstance();
private:
MyClass();
};
Singleton.cpp:
cpp
#include "Singleton.h"
MyClass* MyClass::getInstance()
{
return new MyClass();
}
有了这一层认识,我们接下来剖析经典的单件模式实现。
三.剖析经典的单件模式实现
在写代码之前,我们先理解两个核心知识点,也是C++单件模式的灵魂。
- 类内的静态成员变量,必须在类外初始化 → 是,C++ 硬性语法规则,无例外(除了 const static 整型 / 枚举);
- 类外初始化静态成员时,可以合法调用类的私有构造函数 → 是,核心原理:类自身拥有访问所有私有成员的权限,静态成员属于类本身。
我们首先编写一个懒汉模式的单件模式,也就是延迟实例化的版本。代码如下:
Singleton.h:
cpp
#pragma once
#include <iostream>
using namespace std;
class LazySingleton
{
public:
static LazySingleton* getInstance();
private:
static LazySingleton* instance;
LazySingleton();
LazySingleton(const LazySingleton& another) = delete;
LazySingleton& operator=(const LazySingleton& another) = delete;
};
Singleton.cpp:
cpp
#include "Singleton.h"
LazySingleton* LazySingleton::instance = nullptr;
LazySingleton::LazySingleton()
{
}
LazySingleton* LazySingleton::getInstance()
{
if (instance == nullptr)
{
cout << "第一次实例化" << endl;
instance = new LazySingleton();
}
return instance;
}
首先是防构造,令构造函数成为私有函数,其次是防拷贝构造和赋值运算符重载,避免出现多个对象,这里运用C++11的delete关键字将它俩设置为删除状态。单例对象指针instance私有且在类外初始化为nullptr,getInstance()获取这个单例对象,第一次获取时进行初始化。
当然,这个类是有一些问题的,比如线程安全问题,我们后面细说。
下面再看看恶汉模式的代码实现:
Singleton.h:
cpp
class HungrySingleton
{
public:
static HungrySingleton* getInstance();
private:
static HungrySingleton* instance;
HungrySingleton();
HungrySingleton(const HungrySingleton& another) = delete;
HungrySingleton& operator=(const HungrySingleton& another) = delete;
};
Singleton.cpp:
cpp
HungrySingleton* HungrySingleton::instance = new HungrySingleton();
HungrySingleton::HungrySingleton()
{
}
HungrySingleton* HungrySingleton::getInstance()
{
cout << "获取饿汉实例" << endl;
return instance;
}
与懒汉模式不同的地方在与,单例对象的指针在类外面进行初始化时不是初始化为nullptr,而是实例化出了这个单例对象,在程序启动时这个对象就存在了,持续到进程结束。
现在测试懒汉模式 和饿汉模式:

四.单件模式的告白
"一个"的威力很强的。比如说,你有一个包含注册表设置的对象。你不想让这个对象有多个副本,到处赋值------这会导致混乱。通过使用像我这样的对象,你可以确保应用中的每个对象使用同一全局资源。
单件模式常常被用来管理资源池,像连接或者线程池。
五.定义单件模式
单件模式确保一个类只有一个实例,并提供一个全局访问点。
六.处理多线程
上面的懒汉模式getInstance()函数 的代码不是线程安全的,单例对象是临界资源,我们并没有对它进行加锁保护。

上面是书中用Java多线程场景下导致不同线程获得不同单件对象的例子,咱们C++方向的也能一下看懂。
每次一进入getInstance()后都马上加锁,那么并发就会降低很多。我们用"双重检查加锁"在getInstance()减少使用同步,代码如下:
cpp
LazySingleton* LazySingleton::getInstance()
{
if (instance == nullptr)
{
unique_lock<mutex> ul(_mutex);
if (instance == nullptr)
{
cout << "懒汉第一次实例化" << endl;
instance = new LazySingleton();
}
}
cout << "获取懒汉实例" << endl;
return instance;
}
这样,只有在第一次instance为空时才加锁并且实例化,大大减少了同步,提高了并发,并且线程安全。
七.总结
本章篇幅较小,单例模式也较为简单,但很多细节还是需要我们注意。