Songqian Li's Blog

去历史上留点故事

注:本文引自:https://guodong.plus/2020/0529-002048/

v1.0——基本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
public:
static Singleton& Get() { return s_instance; } // 返回引用
void Function() {}
private:
Singleton() {} // 构造函数设为私有
static Singleton s_instance;
};
Singleton Singleton::s_instance;
int main() {
Singleton::Get().Function();
return 0;
}

因为我们只需要一个 Singleton 实例,所以可以在 Singleton 类中声明一个静态的 Singleton 实例,在类外定义这个实例。然后将构造函数设为私有,这样就只能通过 Get 方法获取 Singleton 的实例,注意这里 Get 返回的是引用,毕竟我们只需要一个 Singleton 实例。

v2.0——解决"多例"问题

第一个版本看似已经不错了,但还是有缺陷的。如果我们的 main 函数是这样:

1
2
3
4
int main() {
Singleton instance = Singleton::Get();
return 0;
}Copy

我们在 main 函数中又定义了一个 Singleton 实例!并且将 Get 获得的静态 Singleton 实例拷贝赋值给了局部自动变量 instance,于是,我们的单例模式就宣告终结了。所以我们还需要将拷贝赋值以及拷贝构造等函数设为删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Singleton {
public:
// 将拷贝构造和拷贝赋值都设为 delete
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

static Singleton& Get() { return s_instance; } // 返回引用

void Function() {}
private:
Singleton() {} // 构造函数设为私有
static Singleton s_instance;
};
Singleton Singleton::s_instance;
int main() {
Singleton& instance = Singleton::Get();
return 0;
}

这样的话,main 函数中第 19 行就必须使用引用了,如果不是引用,编译器就会报错。这样就实现了完整的 Singleton 模式。

v3.0——优化使用体验

假设我们有如下的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Random {
public:
// 将拷贝构造和拷贝赋值都设为 delete
Random(const Random&) = delete;
Random& operator=(const Random&) = delete;
static Random& Get() { return s_instance; } // 返回引用
float Float() { return m_random_generator; }
private:
Random() {} // 构造函数设为私有
static Random s_instance;
float m_random_generator = 0.5f;
};
Random Random::s_instance;
int main() {
float number = Random::Get().Float(); // 非常麻烦的写法
// 我们希望可以用如下方式调用 Float()
// float number = Random::Float();
return 0;
}

这是一个 Random 类,调用 Float 方法可以返回一个随机数(这里假设 m_random_generator 是一个随机数生成器)。于是在 main 函数中,我们需要像 Random::Get().Float() 这样先获得实例,再去调用 Float 方法来获取随机数。Singleton 之所以是 Singleton,是因为它是一个类,可以拥有并使用自己的成员(成员函数使用成员变量 m_random_generator)。不然的话,我们可以直接将 Random 里面的东西都声明为静态的就好了,根本不需要 Singleton(比如将 Random 变成一个 namespace,然后在这个 namespace 中去声明函数和变量)。
然而为了支持类的特性,我们的 Singleton 不得不像上面代码那样非常麻烦的去调用 Get 再调用 Float。我们希望的是只需要像这样Random::Float() 使用即可。那么如何既可以像调用静态方法那样使用 Float 成员函数,又可以让成员函数使用成员变量呢(静态成员函数是不可以使用非静态成员变量的)?
Youtube 的大神给出的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Random {
public:
// 将拷贝构造和拷贝赋值都设为 delete
Random(const Random&) = delete;
Random& operator=(const Random&) = delete;
static Random& Get() { return s_instance; } // 返回引用
/* 这里的 Float 变为了静态函数 */
static float Float() {
/* 将获取实例以及调用成员函数的工作都交给 Float() 静态成员函数来做 */
return Get().IFloat();
}
private:
Random() {} // 构造函数设为私有
static Random s_instance;
/* 将原来的 Float 成员函数设为私有,并更名为 IFloat */
float IFloat() { return m_random_generator; }
float m_random_generator = 0.5f;
};
Random Random::s_instance;
int main() {
cout << Random::Float() << endl;
return 0;
}

是不是很聪明?只是又抽象了一层而已,我们将原来的 Float 成员函数设为私有,并更名为 IFloat(可以理解为 Internal Float 或者 Interface Float 或者 Implement Float),然后将 Float 方法以静态成员函数的形式暴露给调用者,之前获取实例的 Get 步骤本应该由 Float 的调用者去负责,现在我们将这一步交给了 Float 自己去做,并将 Float 变为了一个静态成员函数。
于是我们现在使用 Random 中的方法,就可以全部通过 Random::Method() 来操作了,而不需要自己去将 Random 进行实例化或者是需要先获取 Random 实例了。并且我们还拥有了所有类的特性(在方法中使用成员变量等)。

v4.0——线程安全优化

可以看到为了实现单例模式,我们在类中声明了一个静态的 Random 实例,然后在另外一个地方去定义了这个实例。这种写法会存在一些问题,比如在哪里定义这个实例比较合适?什么时候定义它比较合适?有些时候(比如多线程环境下)这些问题非常的关键。于是就有了以下写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Random {
public:
// 将拷贝构造和拷贝赋值都设为 delete
Random(const Random&) = delete;
Random& operator=(const Random&) = delete;
static Random& Get() {
static Random s_instance;
return s_instance;
} // 返回引用
/* 这里的 Float 变为了静态函数 */
static float Float() {
/* 将获取实例以及调用成员函数的工作都交给 Float() 静态成员函数来做 */
return Get().IFloat();
}
private:
Random() {} // 构造函数设为私有
/* 将原来的 Float 成员函数设为私有,并更名为 IFloat */
float IFloat() { return m_random_generator; }
float m_random_generator = 0.5f;
};
int main() {
cout << Random::Float() << endl;
return 0;
}

为了让代码更简洁、更安全,我们将之前的静态 Random 实例的声明以及定义放在了 Get 方法中,让其成为一个局部静态变量,这样的话,我们在第一次调用 Get 方法的时候,它就会实例化,并且之后调用 Get 方法,使用的都是同一个实例。
这样的写法除了让代码更简洁,还有可以让代码更安全。我们不用担心在使用 Random 的时候,Random 有没有被实例化,因为第一次调用 Get 方法时,我们就顺带进行了实例化操作。并且在 C++ 11 中,局部的静态变量是线程安全的(除非在编译时使用了 --fno-threadsafe-statics 参数),而单纯的静态成员变量是非线程安全的,非线程安全引起的 bug 有时可是很难发现的。

相关文章
评论
分享
  • 右值引用

    本文整理自《modeln C++ Tutorial:C++11/14/17/20》 右值引用是 C11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C 中大 量的历史遗留问题,消除了诸如 std::vecto...

    右值引用
  • 为什么Proactor比Reactor模式更优?

    首先,我们先了解一下什么是 Reactor 模式和 Proactor 模式。 什么是 Reactor 模式和 Proactor 模式? Reactor 模式 Reactor 模式是指主线程负责监听和分发事件,工作线程负责 I/O ...

    为什么Proactor比Reactor模式更优?
  • 容易忽视的C++知识

    格式化输出的执行顺序 由于 C 语言参数压栈顺序是从右往左,所以 printf 和 cout 函数在执行时是从右往左读入缓冲区,然后从左往右输出。 右值引用和左值引用 C++一共有三种基本值类型: 左值(lvalue), 纯右值...

    容易忽视的C++知识
  • 《操作系统真象还原》:第十章 输入输出系统

    上一章中我们遇到的字符混乱和 GP 异常问题,根本原因是由于临界区代码的资源竞争,这需要一些互斥的方法来保证操作的原子性。 10.1 同步机制——锁 10.1.1 排查 GP 异常,理解原子操作 多线程执行刷屏时光标值越界导致...

    《操作系统真象还原》:第十章 输入输出系统
  • 《操作系统真象还原》:第九章 线程

    线程和进程将分两部分实现,本章先讲解线程。 9.1 实现内核线程 9.1.1 执行流 在处理器数量不变的情况下,多任务操作系统采用多道程序设计的方式,使处理器在所有任务之间来回切换,这称为“伪并行”,由操作系统中的任务调度器决定当...

    《操作系统真象还原》:第九章 线程
  • GPU虚拟化

    用户层虚拟化 本地 API 拦截和 API formwarding 在用户态实现一个函数库,假设叫 libwrapper, 它要实现底层库的所有 API; 让 APP 调用这个 libwrapper。如何做? libwrap...

    GPU虚拟化
  • 硬件虚拟化

    硬件虚拟化介绍 硬件虚拟化要做的事情 体系结构支持 体系结构 实现功能 作用 模式切换 Host CPU <-> Guest CPU 切换 CPU 资源隔离 二阶段地址转换 GVA-> GPA...

    硬件虚拟化
  • 《操作系统真象还原》:第八章 内存管理系统

    8.1 makefile 简介 这部分可参考阮一峰的讲解:https://www.ruanyifeng.com/blog/2015/02/make.html 8.1.1 makefile 是什么 makefile 是 Linu...

    《操作系统真象还原》:第八章 内存管理系统
  • 《操作系统真象还原》:第七章 中断

    7.1 中断是什么,为什么要有中断 运用中断能够显著提升并发,从而大幅提升效率。 7.2 操作系统是中断驱动的 略 7.3 中断分类 把中断按事件来源分类,来自 CPU 外部的中断就称为外部中断,来自 CPU 内部的中断称为内部...

    《操作系统真象还原》:第七章 中断
  • 《操作系统真象还原》:第六章 完善内核

    6.1 函数调用约定简介 咱们实验使用cdecl。这里提一下stdcall,cdecl与stdcall的区别在于由谁来回收栈空间。 stdcall是被调用者清理参数所占的栈空间。 举例来说: 12int subtract(int ...

    《操作系统真象还原》:第六章 完善内核