设计模式
单例模式(Singleton)
单例模式确保:
- 一个类只有一个实例
- 全局可访问这个实例
典型用途:日志系统、配置管理器、数据库连接池、线程池等。
“懒汉式”与“饿汉式”的区别
饿汉式: 程序启动时就创建实例, 简单、线程安全(初始化时机唯一)可能浪费资源
懒汉式: 第一次使用时才创建实例,延迟加载、节省资源,需要考虑线程安全
Meyers' Singleton
class Singleton {
private:
Singleton() {} // 私有构造
~Singleton() {} // 可选:防止外部析构
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
public:
static Singleton& getInstance() {
static Singleton instance; // C++11保证线程安全初始化
return instance;
}
};
问题
- 创建singleton实例可能会失败,这个时候怎么保证异常安全[solved]
- 用new+atexit创建还是用uniqueptr[solved]
- 多线程访问的安全, 什么时候变量要加atomic
- 静态析构的顺序会有什么问题[solved]
- callonce什么时候用[solved]
- 怎么证明一个类是线程安全的
- 什么时候要用到更精细地控制销毁时机[solved]
- 什么时候出现多个单例[solved]
- C++ 运行时库是什么
什么时候要用到更精细地控制销毁时机
C++ 程序结束时,静态变量的析构是由 C++ 运行时库 (CRT, C Runtime Library) 自动管理的。核心机制可以总结为:“登记制” + “后进先出 (LIFO)”。
这段代码会调用 C 语言标准库的一个内部函数(通常是 std::atexit 或编译器特定的 __cxa_atexit),将该对象的析构函数注册到一个全局的函数指针栈中。
- 场景:假设你有两个单例:
Logger(日志器)和Database(数据库)。但在Database的析构函数中,可能需要写一条日志:“数据库连接已关闭”。如果此时Logger已经被销毁了,程序就会崩溃或发生未定义行为。 - 你的单例管理着巨大的内存块(例如 1GB 的纹理缓存)如果这些资源只在程序的某个阶段(比如“游戏关卡加载”或“初始化阶段”)使用,而程序后续还需要运行很长时间。如果不手动销毁,这些资源会一直被占用直到程序彻底退出
- 单例定义在一个动态链接库(DLL / .so)中,主程序会动态加载和卸载这个库。如果单例对象是静态局部变量,它的析构函数通常注册在
atexit中,试图在主程序退出时执行。但此时插件的代码内存早已不存在,调用析构函数会导致 Crash。
返回Reference 和返回指针的区别
reference不能表示空,指针可以表示空,允许构造失败。不能使用 std::call_once(因为它一旦执行完就会标记为“已完成”,哪怕你吞掉了异常)。
我们需要使用 Double-Checked Locking (DCL) 配合 try-catch,这样如果初始化失败,下次调用还能再次尝试初始化。
手动控制销毁:不利用 atexit 自动销毁,而是提供一个静态的 destroy() 方法,让你在 main 函数结束前按你想要的顺序手动“点名”销毁。
意外多例
场景:头文件里写了函数内静态(Meyers Singleton),但被编进多个动态库. 如果 可执行文件 和 某个 DLL/.so 都各自编译并链接了这段 instance(),并且符号/链接方式导致它们各自保留一份静态变量,那么:
- EXE 里有一个
S - DLL 里也有一个
S你在不同模块里调用instance(),拿到的不是同一个对象。
实现
#include <expected>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
struct InitError {
int code = 0;
std::string message;
};
// 你的单例类型
class Service {
public:
// 对外:获取单例(可能失败)
static std::expected<Service&, InitError> get() {
std::call_once(s_once, [] {
s_result = create(); // 只执行一次:成功 or 失败都会被缓存
});
if (!s_result.has_value()) {
return std::unexpected(s_result.error());
}
return *s_result.value();
}
// 示例接口
int doWork() const { return 42; }
Service(const Service&) = delete;
Service& operator=(const Service&) = delete;
private:
Service() = default;
// 初始化可能失败的创建逻辑
static std::expected<std::unique_ptr<Service>, InitError> create() {
// 这里写真实初始化:打开文件/连接数据库/创建GPU资源等
bool ok = false; // 示例:让它失败
if (!ok) {
return std::unexpected(InitError{1, "Failed to initialize Service (demo)"});
}
auto p = std::unique_ptr<Service>(new Service());
// 继续做更多初始化,失败就 return unexpected(...)
return p;
}
private:
inline static std::once_flag s_once;
inline static std::expected<std::unique_ptr<Service>, InitError> s_result =
std::unexpected(InitError{0, "Not initialized yet"}); // 初始占位
};
// 用法示例
// auto r = Service::get();
// if (!r) { printf("err: %d %s\n", r.error().code, r.error().message.c_str()); }
// else { printf("%d\n", r->doWork()); }
实现
#include <memory>
#include <mutex>
#include <string>
class Service {
public:
static Service* get(std::string* err = nullptr) {
std::call_once(s_once, [] {
s_instance = create(s_error);
s_inited = true; // 表示“已经尝试过初始化”(无论成败)
});
if (!s_instance && err) *err = s_error;
return s_instance.get();
}
int doWork() const { return 42; }
Service(const Service&) = delete;
Service& operator=(const Service&) = delete;
private:
Service() = default;
static std::unique_ptr<Service> create(std::string& err) {
bool ok = false; // 示例:让它失败
if (!ok) {
err = "Failed to initialize Service (demo)";
return nullptr;
}
return std::unique_ptr<Service>(new Service());
}
private:
inline static std::once_flag s_once;
inline static std::unique_ptr<Service> s_instance{};
inline static std::string s_error{};
inline static bool s_inited = false; // 可选:用于调试/诊断
};
Factory Method工厂模式
业务代码只依赖抽象:AbstractFactory + AbstractProduct。
当你有多种产品,而且这些产品是成套出现(产品族)的:
- 例子:UI 组件一套:
Button / TextBox / CheckBox - 你有不同风格(产品族):
Windows 风格一套、Mac 风格一套 - 业务代码希望:换主题/平台时,一次性换整套组件,并且保证组件之间“配套”。
如果不用抽象工厂,你可能会在代码里到处写:
if (platform == Windows) new WinButton();
else new MacButton();
这会导致业务代码到处都是分支、很难维护。
#include <iostream>
#include <memory>
#include <string>
// ====== Abstract Products ======
struct Button {
virtual ~Button() = default;
virtual void paint() const = 0;
};
struct TextBox {
virtual ~TextBox() = default;
virtual void paint() const = 0;
};
// ====== Concrete Products: Windows ======
struct WinButton : Button {
void paint() const override { std::cout << "[Win] Button\n"; }
};
struct WinTextBox : TextBox {
void paint() const override { std::cout << "[Win] TextBox\n"; }
};
// ====== Concrete Products: Mac ======
struct MacButton : Button {
void paint() const override { std::cout << "[Mac] Button\n"; }
};
struct MacTextBox : TextBox {
void paint() const override { std::cout << "[Mac] TextBox\n"; }
};
// ====== Abstract Factory ======
struct GUIFactory {
virtual ~GUIFactory() = default;
virtual std::unique_ptr<Button> createButton() const = 0;
virtual std::unique_ptr<TextBox> createTextBox() const = 0;
};
// ====== Concrete Factories ======
struct WinFactory : GUIFactory {
std::unique_ptr<Button> createButton() const override {
return std::make_unique<WinButton>();
}
std::unique_ptr<TextBox> createTextBox() const override {
return std::make_unique<WinTextBox>();
}
};
struct MacFactory : GUIFactory {
std::unique_ptr<Button> createButton() const override {
return std::make_unique<MacButton>();
}
std::unique_ptr<TextBox> createTextBox() const override {
return std::make_unique<MacTextBox>();
}
};
// ====== Client Code ======
void renderUI(const GUIFactory& factory) {
auto btn = factory.createButton();
auto tb = factory.createTextBox();
// 客户端完全不关心具体类型,只用抽象接口
btn->paint();
tb->paint();
}
int main(int argc, char** argv) {
// 运行时决定使用哪个产品族(Windows or Mac)
std::unique_ptr<GUIFactory> factory;
std::string platform = (argc > 1 ? argv[1] : "win");
if (platform == "mac") {
factory = std::make_unique<MacFactory>();
} else {
factory = std::make_unique<WinFactory>();
}
renderUI(*factory);
return 0;
}
建造者模式Builder
#include <string>
#include <map>
#include <optional>
#include <stdexcept>
struct HttpRequest {
std::string url;
std::string method = "GET";
std::map<std::string, std::string> headers;
std::string body;
int timeout_ms = 3000;
};
class HttpRequestBuilder {
public:
explicit HttpRequestBuilder(std::string url) : req_{std::move(url)} {}
HttpRequestBuilder& method(std::string m) {
req_.method = std::move(m);
return *this;
}
HttpRequestBuilder& header(std::string k, std::string v) {
req_.headers.emplace(std::move(k), std::move(v));
return *this;
}
HttpRequestBuilder& body(std::string b) {
req_.body = std::move(b);
return *this;
}
HttpRequestBuilder& timeoutMs(int t) {
req_.timeout_ms = t;
return *this;
}
HttpRequest build() const {
// 统一校验点:必填字段、组合约束等
if (req_.url.empty()) throw std::invalid_argument("url is empty");
return req_;
}
private:
HttpRequest req_;
};
// 用法:
// auto req = HttpRequestBuilder("https://api.xxx.com")
// .method("POST")
// .header("Content-Type","application/json")
// .body("{...}")
// .timeoutMs(5000)
// .build();
原型模式
1)跳过昂贵初始化
复杂对象的“创建”往往不只是 new:
- 解析大配置 / JSON / XML
- 构建复杂数据结构(大 vector、索引、哈希表、图结构)
- 预计算缓存、生成查找表
- 初始化一堆子对象、做一致性校验
这些步骤通常是 O(n) 甚至更高,而且包含大量逻辑分支
适用场景(常见面试点):
- 创建对象成本很高(初始化复杂、要读配置/解析、要建大量内部结构)
- 你想要很多“差不多”的对象,只是少量字段不同
- 希望创建过程与具体类解耦:客户端只拿到一个基类指针,通过
clone()得到新对象
一句话:
先造一个“模板对象”,之后都从它复制,再做少量修改。
桥接模式
如果你有两个维度都在变化:
- 维度 A:抽象层(比如“形状:圆/矩形/三角形”)
- 维度 B:实现层(比如“渲染方式:OpenGL/DirectX/Vulkan”)
不用桥接时,常见写法是继承组合:
OpenGLCircle,OpenGLRectangleDirectXCircle,DirectXRectangle- ……
这会导致类数量 = A × B,扩展任何一边都要新增一堆类。
桥接用组合替代这种继承笛卡尔积:
- 抽象层(Shape)持有一个实现层接口(Renderer*)
- 形状新增不影响渲染器新增,反之亦然
Strategy
#include <iostream>
#include <functional>
class Cashier {
public:
using Strategy = std::function<double(double)>;
explicit Cashier(Strategy s) : strategy_(std::move(s)) {}
void setStrategy(Strategy s) { strategy_ = std::move(s); }
double checkout(double base) const { return strategy_(base); }
private:
Strategy strategy_;
};
int main() {
Cashier c([](double x){ return x; });
std::cout << c.checkout(100) << "\n"; // 100
c.setStrategy([](double x){ return x * 0.9; });
std::cout << c.checkout(100) << "\n"; // 90
}
装饰(Decorator)模式
装饰(Decorator)模式:在不改原类代码、也不靠继承爆炸的情况下,给对象动态叠加功能。它的关键词是:包装(wrap) + 叠加(stackable) + 同接口(is-a)。
#include <iostream>
#include <memory>
#include <string>
// ===== Component =====
struct Beverage {
virtual ~Beverage() = default;
virtual std::string desc() const = 0;
virtual double cost() const = 0;
};
// ===== ConcreteComponent =====
struct Espresso : Beverage {
std::string desc() const override { return "Espresso"; }
double cost() const override { return 12.0; }
};
// ===== Decorator Base =====
class CondimentDecorator : public Beverage {
public:
explicit CondimentDecorator(std::unique_ptr<Beverage> inner)
: inner_(std::move(inner)) {}
protected:
std::unique_ptr<Beverage> inner_;
};
// ===== ConcreteDecorators =====
class Milk : public CondimentDecorator {
public:
using CondimentDecorator::CondimentDecorator;
std::string desc() const override { return inner_->desc() + " + Milk"; }
double cost() const override { return inner_->cost() + 2.0; }
};
class Sugar : public CondimentDecorator {
public:
using CondimentDecorator::CondimentDecorator;
std::string desc() const override { return inner_->desc() + " + Sugar"; }
double cost() const override { return inner_->cost() + 1.0; }
};
int main() {
std::unique_ptr<Beverage> drink = std::make_unique<Espresso>();
drink = std::make_unique<Milk>(std::move(drink));
drink = std::make_unique<Sugar>(std::move(drink));
drink = std::make_unique<Sugar>(std::move(drink)); // 还能再叠一层
std::cout << drink->desc() << "\n";
std::cout << "Cost: " << drink->cost() << "\n";
}
Observer
场景:一对多通知:GUI 事件、订阅/发布、配置更新、游戏事件等。
class Observer {
public:
virtual ~Observer() = default;
virtual void onNotify(int data) = 0;
};
class Subject {
public:
void addObserver(Observer* o) { obs_.push_back(o); }
void notify(int data) {
for (auto o : obs_) o->onNotify(data);
}
private:
std::vector<Observer*> obs_;
};
Reactor pattern
在有成千上万连接时,避免“一个连接一个线程”的资源浪费,用事件驱动 + 非阻塞 IO来处理大量并发连接。
Proactor Pattern
Proxy Pattern
代理模式(Proxy Pattern):为某个对象提供一个替身/中介(代理),由代理控制对真实对象的访问。对外接口不变,调用方“看起来”在用目标对象,实际上请求会先经过代理