C++常用设计模式高效学习详解
内容来自:程序员老廖的个人空间
第1章 单例模式(C++)
单例模式的描述是: 确保一个类只有一个实例,并提供对该实例的全局访问。
单例模式的最重要特点就是:一个类最多只有一个对象。 意图:确保系统中某类只有唯一实例,并提供全局访问点。
动机:全局配置、日志器、线程池、连接池等只需一份共享状态与资源,避免重复创建、减少竞争与内存浪费。
类图(核心角色)
时序图:多线程首次访问
实现要点
- 线程安全与内存可见性是面试核心
- C++11 起,函数内局部静态变量初始化为线程安全
- DCLP 需要正确的 memory order(acquire/release)与指针发布
- std::call_once 保证初始化只执行一次
核心代码(五种实现对比)
仅展示关键片段,完整可运行代码见 src/。
// B站程序员老廖
// 1. Meyers 单例(推荐)
class MeyersSingleton {
public:
static MeyersSingleton& instance() {
static MeyersSingleton instance; // C++11 线程安全
return instance;
}
void doWork();
};
// 2. DCLP(双重检查锁)
class DCLPSingleton {
public:
/*
// 这个是错误写法
if (!instance_) {
lock;
if (!instance_) {
instance_ = new X;
}
}
*/
// 双重检查锁(DCLP)的目标
static DCLPSingleton* instance() {
DCLPSingleton* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) { // 只有在实例未创建时,才进入加锁区
std::lock_guard<std::mutex> lock(mutex_); // 加锁后再检查一次(防止多个线程同时进入创建)
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new DCLPSingleton();
// “释放”这个指针,表示“我准备好了”
instance_.store(tmp, std::memory_order_release); // 保证:上面所有操作(包括构造)都已完成
}
}
return tmp; //第一次检查:无锁,快速返回已有实例
}
// 普通加锁方案的问题 每次调用都要加锁,性能差
static DCLPSingleton* instance_common() {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) {
instance_ = new DCLPSingleton();
}
return instance_;
}
private:
static std::atomic<DCLPSingleton*> instance_;
static std::mutex mutex_;
};
// 3. call_once 单例
class CallOnceSingleton {
public:
static CallOnceSingleton& instance() {
std::call_once(flag_, [](){ instance_.reset(new CallOnceSingleton()); });
return *instance_;
}
private:
static std::once_flag flag_;
static std::unique_ptr<CallOnceSingleton> instance_;
};
// 4. 饿汉式单例(程序启动时立即初始化)
class EagerSingleton {
public:
static EagerSingleton& instance() {
return instance_; // 直接返回,无需检查
}
private:
static EagerSingleton instance_; // 程序启动时构造
};
// 5. 懒汉式单例(首次使用时初始化,互斥锁保证线程安全)
class LazySingleton {
public:
static LazySingleton& instance() {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) {
instance_.reset(new LazySingleton());
}
return *instance_;
}
private:
static std::unique_ptr<LazySingleton> instance_;
static std::mutex mutex_;
};
双重检查锁定(Double-Checked Locking Pattern, DCLP
DCLP 的作用与背景
1. 目标
实现一个线程安全的单例模式(Singleton),并且在首次创建后,后续调用 instance() 时不加锁、高性能。
2. 普通加锁方案的问题
static DCLPSingleton* instance() {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) {
instance_ = new DCLPSingleton();
}
return instance_;
}
✅ 正确 ❌ 每次调用都要加锁,性能差
3. 双重检查锁(DCLP)的目标
- 第一次检查:无锁,快速返回已有实例
- 只有在实例未创建时,才进入加锁区
- 加锁后再检查一次(防止多个线程同时进入创建)
- 创建后存储实例
这就是“双重检查”的由来。
为什么需要 std::atomic 和 内存序?
这是最容易被误解的部分。我们来看原始代码:
static DCLPSingleton* instance() {
DCLPSingleton* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new DCLPSingleton();
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
1. 为什么 instance_ 必须是 std::atomic<T*>?
因为多个线程会并发读写 instance_ 指针。
- 如果不用 atomic,多个线程同时读写非原子变量 → 数据竞争(data race) → 未定义行为(UB)
- std::atomic 提供了原子性保证,避免读写撕裂(tearing)
✅ 所以 atomic 是必须的,不是可选项。
2. 为什么需要 memory_order_acquire 和 memory_order_release?
这才是最精妙的地方。我们来一步步拆解。
❌ 问题:编译器/CPU 会重排序!
考虑这行代码:
tmp = new DCLPSingleton(); instance_.store(tmp, std::memory_order_release);
在底层,new DCLPSingleton() 包含两个动作:
- 分配内存
- 调用构造函数初始化对象
但编译器或 CPU 可能重排序这些操作,导致:
指针 instance_ 先被赋值,但对象还没构造完成!
此时,另一个线程执行:
DCLPSingleton* tmp = instance_.load(); // 看到了非空指针
if (!tmp) { ... } // 跳过
return tmp; // 返回一个“半成品”对象!崩溃!
这就是典型的重排序导致的安全问题。
解决方案:用 acquire-release 内存序建立“同步关系”
我们使用:
- load(std::memory_order_acquire):获取操作,确保后续读写不会被重排到它前面
- store(std::memory_order_release):释放操作,确保前面的读写不会被重排到它后面
当一个线程执行 store(release),另一个线程执行 load(acquire) 并读到了这个值,就形成了 synchronizes-with 关系。
这意味着:所有在 release 前的写操作,对 acquire 后的读操作可见。
举个例子:
线程 A(创建):
tmp = new DCLPSingleton(); // 构造完成 instance_.store(tmp, release); // 发布指针
线程 B(读取):
tmp = instance_.load(acquire); // 读到非空 // 由于 acquire-release 同步,保证能看到构造完成的对象!
✅ 安全!
为什么第二次检查用 relaxed?
tmp = instance_.load(std::memory_order_relaxed);
因为在持有 mutex 锁的情况下,mutex 本身已经提供了同步语义(mutex 是 acquire/release 的)。
所以这里的 load 只是为了判断是否需要创建,不需要额外的内存序约束,用 relaxed 更高效。
面试高频与深入剖析
为什么 Meyers 单例在 C++11 后默认线程安全?
- 函数内静态变量的初始化由运行时提供“magic static”一次性、线程安全机制,并建立初始化与后续读取的 happens-before。
DCLP(Double-Checked Locking Pattern,双重检查锁定模式)如何写才安全?
- 使用 std::atomic<T*> 指针;发布侧 store(memory_order_release),读取侧 load(memory_order_acquire);锁内读可用 relaxed;避免半初始化与指令重排。
call_once 与 Meyers 的取舍?
- Meyers 简洁、懒加载、零样板;call_once 便于自定义销毁与资源管理(如 unique_ptr)与显式初始化点。
静态析构顺序如何处理?
- 跨 TU 析构次序不定:可选择不析构(服务进程)、使用 std::atexit 注册销毁、或提供 shutdown() 明确释放顺序。
受控销毁代码片段(跨 TU 析构次序)
// B站程序员老廖
#include <cstdlib>
#include "singleton/singleton.hpp"
// 在进程退出时按顺序销毁单例,避免跨 TU 析构次序不定
static void destroy_singletons_in_order() {
CallOnceSingleton::shutdown(); // 先释放可控资源(unique_ptr)
DCLPSingleton::destroy(); // 再释放 DCLP 指针实例
// MeyersSingleton 通常不主动析构(由 OS 回收),避免析构次序风险
}
int main() {
std::atexit(destroy_singletons_in_order);
// ... 业务逻辑
}
运行与验证
见根目录 README 的构建命令。可执行程序会在多线程下对三种实现各执行 9 次 doWork(),并断言计数。
反例:非线程安全懒汉(用于说明问题)
class UnsafeLazySingleton {
public:
static UnsafeLazySingleton* instance() {
if (!instance_) instance_ = new UnsafeLazySingleton();
return instance_;
}
};
多个线程同时通过空检查可能创建多个实例,且存在发布-逸出问题。
实战映射
- 日志器、配置加载、线程池、连接池
- 多模块共享监控/指标上报器(例如全局 MetricsRegistry)
真实案例:线程安全日志器单例(C++)
代码位置:src/logger/logger.hpp、src/logger/examples_logger.cpp
类图
时序图:客户端并发写日志
核心使用代码(片段)
仅展示要点,完整示例见 src/logger/examples_logger.cpp
// B站程序员老廖
#include "logger/logger.hpp"
#include <thread>
#include <vector>
int main() {
Logger::instance().logInfo("启动服务");
std::vector<std::thread> ws;
for (int i = 0; i < 4; ++i) {
ws.emplace_back([i]{
Logger::instance().logInfo("worker " + std::to_string(i) + " 开始");
Logger::instance().logError("worker " + std::to_string(i) + " 警告");
});
}
for (auto& t : ws) t.join();
Logger::instance().logInfo("服务退出");
}
面试速答清单
Meyers 单例为什么线程安全?
- C++11 magic static 提供一次性、线程安全初始化并建立可见性。
DCLP 全称与中文?
- Double-Checked Locking Pattern,双重检查锁定模式。
DCLP 的可见性如何保证?
- 原子指针 + 发布侧 release、读取侧 acquire;锁内读可用 relaxed。
如何避免静态析构次序问题?
- 不析构或 atexit 管理,或提供 shutdown() 明确销毁顺序。
第2章 工厂方法模式(C++)
工厂方法模式的描述是: 定义创建对象的接口,让子类决定实例化哪个类。工厂方法使类的实例化延迟到子类。
工厂方法模式的最重要特点就是:将对象创建延迟到子类,符合开闭原则。
意图:定义一个创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
动机:当系统需要创建多种相关产品,且产品种类可能扩展时,直接在客户端 new 会导致代码耦合。工厂方法通过抽象工厂接口,让具体工厂负责创建具体产品,支持无修改扩展。
类图(分解展示:简单工厂 vs 工厂方法)
1. 简单工厂模式结构
2. 工厂方法模式结构
3. 产品继承关系
时序图:工厂方法创建产品
实现要点
- 工厂方法将对象创建延迟到子类,每个具体工厂负责创建对应产品
- 符合开闭原则:新增产品只需添加具体工厂和具体产品,无需修改现有代码
- 与简单工厂对比:简单工厂集中创建但违反开闭原则,工厂方法分散创建但易扩展
- 可结合模板方法模式,在抽象工厂中定义通用业务流程
核心代码(简单工厂 vs 工厂方法 vs 参数化工厂)
仅展示关键片段,完整可运行代码见 src/factory/。
// B站程序员老廖
// 抽象产品
class Logger {
public:
virtual ~Logger() = default;
virtual void log(const std::string& message) = 0;
virtual std::string getType() const = 0;
};
// 具体产品
class FileLogger : public Logger {
public:
explicit FileLogger(const std::string& filename) : filename_(filename) {}
void log(const std::string& message) override {
std::cout << "[FILE:" << filename_ << "] " << message << std::endl;
}
std::string getType() const override { return "FileLogger"; }
private:
std::string filename_;
};
// 1. 简单工厂(违反开闭原则)
class SimpleLoggerFactory {
public:
enum class LoggerType { FILE, CONSOLE, NETWORK };
static std::unique_ptr<Logger> createLogger(LoggerType type, const std::string& param = "") {
switch (type) {
case LoggerType::FILE: return std::make_unique<FileLogger>(param);
case LoggerType::CONSOLE: return std::make_unique<ConsoleLogger>();
// 新增类型需要修改此处,违反开闭原则
}
}
};
// 2. 工厂方法(符合开闭原则)
class LoggerFactory {
public:
virtual ~LoggerFactory() = default;
virtual std::unique_ptr<Logger> createLogger() = 0; // 延迟到子类
// 模板方法:使用工厂方法的通用业务逻辑
void processLog(const std::string& message) {
auto logger = createLogger();
logger->log("Processing: " + message);
}
};
class FileLoggerFactory : public LoggerFactory {
public:
explicit FileLoggerFactory(const std::string& filename) : filename_(filename) {}
std::unique_ptr<Logger> createLogger() override {
return std::make_unique<FileLogger>(filename_);
}
private:
std::string filename_;
};
// 3. 参数化工厂(兼顾便利性和扩展性)
class ParameterizedLoggerFactory {
public:
using CreateFunc = std::function<std::unique_ptr<Logger>(const std::string&)>;
static void registerLogger(const std::string& type, CreateFunc creator) {
creators_[type] = creator; // 支持运行时注册新类型
}
static std::unique_ptr<Logger> createLogger(const std::string& type, const std::string& param = "") {
auto it = creators_.find(type);
return it != creators_.end() ? it->second(param) : nullptr;
}
private:
static std::unordered_map<std::string, CreateFunc> creators_;
};
面试高频与深入剖析
简单工厂 vs 工厂方法的核心区别?
- 简单工厂:集中创建逻辑,通过参数决定创建哪种产品,但新增产品需修改工厂类(违反开闭原则)。工厂方法:将创建延迟到子类,每个具体工厂创建对应产品,新增产品只需添加工厂子类(符合开闭原则)。
为什么工厂方法更易扩展?
- 工厂方法遵循"对扩展开放,对修改关闭"原则。新增产品类型时,只需新增具体产品类和具体工厂类,无需修改已有代码,降低了系统耦合度和引入bug的风险。
工厂方法的缺点是什么?
- 类的数量成倍增长(每个产品需要对应的工厂类),增加了系统复杂度。对于产品种类较少且变化不频繁的场景,简单工厂可能更合适。
如何选择工厂模式?
- 产品种类固定且少:简单工厂。产品种类多且需要扩展:工厂方法。需要运行时动态注册:参数化工厂(结合注册器模式)。
运行与验证
见根目录 README 的构建命令。可执行程序会演示三种工厂模式的使用方式,包括运行时注册新产品类型的扩展性验证。
真实案例:日志系统工厂(C++)
代码位置:src/factory/factory.hpp、src/factory/examples.cpp
业务场景类图
工厂类继承关系
产品类继承关系
核心使用代码(片段)
仅展示要点,完整示例见 src/factory/examples.cpp
// B站程序员老廖
#include "factory/factory.hpp"
int main() {
// 1. 简单工厂使用
auto logger1 = SimpleLoggerFactory::createLogger(
SimpleLoggerFactory::LoggerType::FILE, "app.log");
logger1->log("简单工厂创建的日志");
// 2. 工厂方法使用
std::unique_ptr<LoggerFactory> factory =
std::make_unique<FileLoggerFactory>("method.log");
factory->processLog("工厂方法处理的消息");
// 3. 参数化工厂使用(支持运行时扩展)
auto logger2 = ParameterizedLoggerFactory::createLogger("file", "param.log");
// 运行时注册新类型
ParameterizedLoggerFactory::registerLogger("debug",
[](const std::string& level) -> std::unique_ptr<Logger> {
return std::make_unique<DebugLogger>(level);
});
auto debugLogger = ParameterizedLoggerFactory::createLogger("debug", "VERBOSE");
}
面试速答清单
简单工厂 vs 工厂方法的本质区别?
- 简单工厂集中创建,工厂方法分散创建;前者违反开闭原则,后者符合开闭原则。
工厂方法如何体现开闭原则?
- 新增产品时只需添加具体产品类和具体工厂类,无需修改现有代码。
工厂方法的缺点?
- 类数量成倍增长,增加系统复杂度;适合产品种类多且需扩展的场景。
什么时候用简单工厂?
- 产品种类少且相对固定,追求代码简洁时;如配置文件解析器的格式选择。
内容太多,需要以下章节内容的可以观看以下视频自行领取完整的学习文档
第3章 抽象工厂模式(C++)
第4章 策略模式(C++)
第5章 观察者模式(C++)
第6章 适配器模式(C++)
第7章 责任链模式(C++)
#秋招白月光##后端##秋招##校招##c++#