文章目录
- 一、题目
- 二、考察点
- 三、答案
- 3.1 C++11写法
- 3.2 C++98写法(线程安全只存在于懒汉模式)
- 3.2.1 小菜写法
- 3.2.2 小菜进阶写法
- 3.2.3 中登写法
- 3.2.3 老鸟写法
- 四、扩展知识
- 4.1 饿汉模式和懒汉模式的区别
- 4.1.1 饿汉模式(Eager Initialization)
- 4.1.2 懒汉模式(Lazy Initialization)
- 4.2 类中的成员变量啥时候初始化?
- 4.2.1 普通成员变量(非静态、非 const)
- 4.2.2 静态成员变量(`static`)
- 4.2.3 常量成员变量(`const`)
- 4.2.4 引用成员变量(`&`)
- 4.2.5 总结:核心原则
- 五、整体答案
一、题目
设计一个类,只能生成该类的一个实例。
二、考察点
singleton模式!
单例模式是指实现了特殊模式的类,该类仅能被实例化一次,产生唯一的一个对象。其常见的实现方式有饿汉式、懒汉式、双检锁、静态内部类、枚举等,评价指标包括是否为单例、线程安全、是否支持延迟加载、能否防止反序列化产生新对象以及防止反射攻击等。
三、答案
3.1 C++11写法
#include <iostream>
using namespace std;
class Singleton
{
public:static Singleton* getInstance(){static Singleton* instance;return instance;}
private:Singleton() = default;Singleton(const Singleton& other) = delete;Singleton& operator=(const Singleton& other) = delete;
};int main() {Singleton* s1 = Singleton::getInstance();Singleton* s2 = Singleton::getInstance();// 验证是否为同一个实例cout << (s1 == s2 ? "是单例" : "不是单例") << endl; // 输出:是单例
}
这样的写法是C++11这样写的话,是满足线程安全的,方便快捷!
3.2 C++98写法(线程安全只存在于懒汉模式)
当然我们讨论线程安全,都是基于懒汉模式的!饿汉模式天然具有线程安全这一特性!
3.2.1 小菜写法
#include <iostream>class Singleton {
public:// 首次调用时创建实例static Singleton* getInstance() {if (instance == NULL) {instance = new Singleton();}return instance;}// 测试方法void print() {std::cout << "Singleton instance address: " << this << std::endl;}private:// 私有构造函数Singleton() {}// 禁用拷贝构造和赋值Singleton(const Singleton&);Singleton& operator=(const Singleton&);// 静态指针成员static Singleton* instance;
};
懒汉模式的线程安全问题源于其延迟初始化的特性 —— 实例在首次调用getInstance()
时才创建,而非程序启动时。在多线程环境下,若多个线程同时进入 “未创建实例” 的代码分支,可能导致多个实例被创建,破坏单例的唯一性。
具体过程拆解(以 C++98 懒汉模式为例)
假设懒汉模式的getInstance()
实现如下(简化版):
Singleton* Singleton::getInstance() {if (instance == NULL) { // 检查实例是否已创建instance = new Singleton(); // 创建实例}return instance;
}
多线程并发时,问题可能这样发生:
- 线程 A进入
if (instance == NULL)
判断,发现未创建实例,准备执行new
操作。 - 线程 B在线程 A 执行
new
之前,也进入if
判断,此时instance
仍为NULL
,因此也会执行new
操作。 - 最终,线程 A 和线程 B 各自创建了一个实例,
instance
指针被两次赋值,导致单例被破坏(两个不同的实例同时存在)。
核心原因总结:
- 判断与创建的非原子性:
if (instance == NULL)
和new Singleton()
是两个独立的操作,而非一个不可分割的原子操作。 - 并发抢占:多线程在 “判断为空” 到 “实际创建” 的间隙中可能同时进入临界区,导致重复创建。
这就是为什么懒汉模式在多线程环境下必须通过加锁(如pthread_mutex_lock
)等同步机制保证线程安全,而饿汉模式因实例在程序启动时(单线程阶段)就已创建,天然不存在此问题。
3.2.2 小菜进阶写法
#include <iostream>
#include <mutex>
using namespace std;
mutex mtx;
class Singleton
{
public:static Singleton* getInstance(){if (nullptr == m_instance){mtx.lock();m_instance = new Singleton;mtx.unlock();}return m_instance;}
private:static Singleton* m_instance ;Singleton() = default;Singleton(const Singleton& other) = delete;Singleton& operator=(const Singleton& other) = delete;
};int main() {Singleton* s1 = Singleton::getInstance();Singleton* s2 = Singleton::getInstance();// 验证是否为同一个实例cout << (s1 == s2 ? "是单例" : "不是单例") << endl; // 输出:是单例
}
问题:直接使用 mtx.lock()
和 mtx.unlock()
也存在风险:
-
如果
new Singleton
过程中抛出异常,mtx.unlock()
将不会执行,导致锁永远无法释放(死锁)。 -
正确做法是使用:
lock_guard
(RAII 机制),确保锁在任何情况下都能自动释放:
if (nullptr == m_instance) {lock_guard<mutex> lock(mtx); // 自动加锁,作用域结束时自动解锁// ... 创建实例 ... }
3.2.3 中登写法
#include <iostream>
#include <mutex>
using namespace std;
mutex mtx;
class Singleton
{
public:static Singleton* getInstance(){if (nullptr == m_instance){lock_guard<mutex> lock(mtx);m_instance = new Singleton;}return m_instance;}
private:static Singleton* m_instance ;Singleton() = default;Singleton(const Singleton& other) = delete;Singleton& operator=(const Singleton& other) = delete;
};int main() {Singleton* s1 = Singleton::getInstance();Singleton* s2 = Singleton::getInstance();// 验证是否为同一个实例cout << (s1 == s2 ? "是单例" : "不是单例") << endl; // 输出:是单例
}
问题: 缺少二次检查(双检锁必要步骤)
即使修正了前两个问题,单重检查加锁仍有漏洞:
- 线程 A 检查
m_instance
为空后加锁,在执行new
前被挂起。 - 线程 B 同样检查
m_instance
为空,等待线程 A 释放锁。 - 线程 A 释放锁后,线程 B 获得锁,再次创建实例(导致两个实例)。
解决:加锁后必须再次检查 m_instance
是否为空(双检锁)
3.2.3 老鸟写法
#include <iostream>
#include <mutex>
using namespace std;
mutex mtx;
class Singleton
{
public:static Singleton* getInstance(){if (nullptr == m_instance){lock_guard<mutex> lock(mtx);if (nullptr == m_instance){m_instance = new Singleton;}}return m_instance;}
private:static Singleton* m_instance ;Singleton() = default;Singleton(const Singleton& other) = delete;Singleton& operator=(const Singleton& other) = delete;
};int main() {Singleton* s1 = Singleton::getInstance();Singleton* s2 = Singleton::getInstance();// 验证是否为同一个实例cout << (s1 == s2 ? "是单例" : "不是单例") << endl; // 输出:是单例
}
双检锁机制:
- 第一次检查(
if (nullptr == m_instance)
):未加锁,快速判断实例是否已创建,避免每次调用都加锁,提高性能。 - 第二次检查:加锁后再次判断,防止多个线程同时通过第一次检查后重复创建实例
四、扩展知识
4.1 饿汉模式和懒汉模式的区别
饿汉模式和懒汉模式是单例模式中两种最常见的实现方式,核心区别在于实例创建的时机,以及由此衍生的线程安全、资源效率等差异:
4.1.1 饿汉模式(Eager Initialization)
-
核心特点:程序启动时(类加载阶段)就创建实例,无论后续是否使用。
-
实现示例:
class Singleton { private:// 静态成员变量,类加载时初始化static Singleton instance;// 私有构造函数Singleton() {}// 禁用拷贝和赋值Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:// 直接返回已创建的实例static Singleton& getInstance() {return instance;} };// 类外初始化静态成员(程序启动时执行) Singleton Singleton::instance;
-
优缺点:
- 优点:
- 实现简单,无需考虑线程安全问题(初始化在程序启动时完成,早于多线程启动)。
- 不存在并发访问风险,性能稳定。
- 缺点:
- 提前占用内存,即使程序全程未使用该实例,也会消耗资源(尤其对资源密集型单例不友好)。
- 若单例依赖其他初始化逻辑(如配置文件加载),可能因初始化顺序问题导致错误。
- 优点:
4.1.2 懒汉模式(Lazy Initialization)
-
核心特点:首次使用时才创建实例,延迟到需要时再初始化。
-
实现示例(C++11 线程安全版):
class Singleton { private:// 私有构造函数Singleton() {}// 禁用拷贝和赋值Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:// 首次调用时创建实例(静态局部变量保证线程安全)static Singleton& getInstance() {static Singleton instance;return instance;} };
-
优缺点:
- 优点:
- 按需创建,节省资源(未使用时不占用内存)。
- 初始化顺序灵活,可依赖其他已初始化的资源。
- 缺点:
- 实现相对复杂,需要处理线程安全问题(C++11 前需手动加锁,如双检锁)。
- 首次调用
getInstance()
时可能有性能开销(初始化耗时)。
- 优点:
核心区别对比
维度 | 饿汉模式 | 懒汉模式 |
---|---|---|
实例创建时机 | 类加载 / 程序启动时 | 首次调用getInstance() 时 |
线程安全(天然) | 是(初始化早于多线程) | 否(需额外处理,C++11 后改善) |
资源效率 | 较低(提前占用资源) | 较高(按需分配) |
实现复杂度 | 简单(无需处理并发) | 较复杂(需考虑线程安全) |
适用场景 | 单例体积小、初始化快 | 单例体积大、初始化耗资源 |
总结
- 饿汉模式:“饿” 意味着迫不及待,适合简单、轻量的单例,追求实现简单和线程安全。
- 懒汉模式:“懒” 意味着延迟行动,适合复杂、耗资源的单例,追求资源利用效率。
实际开发中,若单例初始化成本低且肯定会被使用,优先选饿汉模式;若单例可能不被使用或初始化成本高,选懒汉模式(C++11 及以上推荐静态局部变量方式,简洁且线程安全)。
4.2 类中的成员变量啥时候初始化?
类的成员变量初始化时机取决于变量的类型(如普通成员变量、静态成员变量、常量成员变量等)和初始化方式(如默认初始化、显式初始化、构造函数初始化等)。以下是不同场景下的初始化时机总结:
4.2.1 普通成员变量(非静态、非 const)
普通成员变量属于类的实例,其初始化时机与对象的创建绑定,具体分为两种情况:
-
默认初始化(编译器自动处理)
若未显式初始化,编译器会在对象创建时(即构造函数执行期间)对成员变量进行默认初始化:- 对于基本数据类型(如
int
、double
):默认值不确定(局部对象中为随机值,全局 / 静态对象中为 0)。 - 对于类类型(如
string
、自定义类):会调用其默认构造函数初始化。
示例:
class MyClass { private:int a; // 基本类型,默认初始化值不确定(局部对象中)string str; // 类类型,默认调用string()构造函数初始化 };
- 对于基本数据类型(如
-
显式初始化(推荐)
为避免默认初始化的不确定性,通常需要显式初始化,时机包括:-
构造函数初始化列表:在构造函数执行前完成初始化(效率更高,推荐用于所有成员变量)。
class MyClass { private:int a;string str; public:// 初始化列表在构造函数体执行前初始化成员变量MyClass() : a(0), str("default") {} };
-
构造函数体内赋值:在构造函数体执行时对已默认初始化的成员变量重新赋值(效率略低,适合复杂逻辑)。
MyClass() {a = 0; // 先默认初始化a,再赋值str = "default"; // 先默认初始化str,再赋值 }
-
C++11 后:类内初始值:在成员变量声明时直接赋值(编译器会将其放入初始化列表)。
class MyClass { private:int a = 0; // 类内初始值string str = "default"; };
-
4.2.2 静态成员变量(static
)
静态成员变量属于类本身(而非实例),其初始化时机与类的生命周期绑定,与对象创建无关:
-
初始化时机:
- 在类外单独初始化,且只初始化一次(程序启动时,在
main
函数执行前完成)。 - 若未显式初始化,基本类型默认值为 0,类类型调用默认构造函数。
- 在类外单独初始化,且只初始化一次(程序启动时,在
-
注意事项:
- 静态成员变量必须在类外定义(初始化),类内仅声明。
- 局部静态成员变量(如在函数内定义的
static
变量)首次调用函数时初始化,且只初始化一次。
示例:
class MyClass { private:static int count; // 类内声明 }; int MyClass::count = 0; // 类外初始化(程序启动时执行)
4.2.3 常量成员变量(const
)
const
成员变量必须在初始化时赋值,且赋值后不可修改,初始化时机严格限制:
-
普通
const
成员变量:
必须在构造函数初始化列表中初始化(不能在构造函数体内赋值,因为进入函数体时变量已初始化)。示例:
class MyClass { private:const int num; public:MyClass(int n) : num(n) {} // 必须在初始化列表中赋值 };
-
静态
const
成员变量:- 可在类内声明时直接初始化(仅允许基本数据类型或枚举),也可在类外初始化。
- 若为类类型(如
string
),必须在类外初始化。
示例:
class MyClass { private:static const int MAX_SIZE = 100; // 类内初始化(基本类型)static const string NAME; // 类内声明,类外初始化 }; const string MyClass::NAME = "MyClass"; // 类外初始化(类类型)
4.2.4 引用成员变量(&
)
引用必须绑定到一个对象,且一旦绑定不可更改,因此必须在构造函数初始化列表中初始化,与const
成员变量类似。
class MyClass {
private:int& ref;
public:MyClass(int& x) : ref(x) {} // 必须在初始化列表中绑定引用
};
4.2.5 总结:核心原则
- 普通成员变量:在对象创建时初始化,推荐用构造函数初始化列表或类内初始值。
- 静态成员变量:在类外初始化(程序启动时),与对象无关,仅初始化一次。
const
/ 引用成员变量:必须在构造函数初始化列表中初始化(静态const
基本类型可类内初始化)。
遵循这些规则可避免未初始化的变量导致的未定义行为,确保程序正确性。
五、整体答案
最推荐写法:
#include <iostream>
using namespace std;
class Singleton
{
public:static Singleton* getInstance(){static Singleton* instance;return instance;}
private:Singleton() = default;Singleton(const Singleton& other) = delete;Singleton& operator=(const Singleton& other) = delete;
};int main() {Singleton* s1 = Singleton::getInstance();Singleton* s2 = Singleton::getInstance();// 验证是否为同一个实例cout << (s1 == s2 ? "是单例" : "不是单例") << endl; // 输出:是单例
}