一、设计模式是什么?先聊聊我的理解
设计模式,说白了就是前辈们踩过无数坑之后总结出来的「套路」。不是 C++ 特有的,但 C++ 里用起来特别顺手。
我个人习惯把设计模式分成三类:创建型、结构型、行为型。今天咱们聊创建型,就是管「对象怎么生出来」的。你想想看,一个对象 new 出来很简单,但什么时候 new、谁来 new、new 几个,这里头学问大了。
创建型模式的核心思想:把对象的创建和使用解耦。调用方不需要知道对象是怎么造出来的,只需要拿到就能用。
好,咱们一个一个来看。
二、单例模式(Singleton)
2.1 什么是单例?
全局只有一个实例。比如日志系统、配置管理器、线程池——这些东西你不需要两个副本,一个就够了。
我在项目中遇到过一个问题:日志类没做单例,结果两个模块各 new 了一个,写日志时互相覆盖。嗯,从那以后,日志类我必用单例。
2.2 C++ 实现要点
C++ 里实现单例,有几个关键点:
- 构造函数私有化——不让外面 new
- 拷贝构造和赋值操作要 delete 掉
- 提供一个静态方法获取实例
最简单的写法(饿汉式):
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
~Singleton() {}
};
小提示:C++11 之后,局部 static 变量是线程安全的。所以上面这个写法在多线程环境下没问题。我以前用老标准时还得加锁,现在省心了。
2.3 注意事项
- 不要滥用单例。我见过有人把每个类都做成单例,那还不如写全局变量呢。
- 单例的析构时机要小心。如果单例 A 依赖单例 B,析构顺序不可控,可能 crash。
- 测试时单例比较麻烦,因为状态是全局的。可以考虑用依赖注入来替代。
我曾经踩过的坑:在单例的析构函数里调用了另一个单例的方法,结果那个单例已经被销毁了。程序退出时崩溃,查了两天才找到原因。建议:单例之间不要互相依赖,或者用智能指针管理生命周期。
三、工厂模式(Factory)
3.1 简单工厂
一个工厂类,根据参数返回不同的产品对象。说白了就是一个 switch-case 或者 if-else。
enum class ProductType { A, B };
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};
class ProductA : public Product {
public:
void use() override { std::cout << "Product A\n"; }
};
class ProductB : public Product {
public:
void use() override { std::cout << "Product B\n"; }
};
class SimpleFactory {
public:
static std::unique_ptr<Product> create(ProductType type) {
switch (type) {
case ProductType::A: return std::make_unique<ProductA>();
case ProductType::B: return std::make_unique<ProductB>();
default: return nullptr;
}
}
};
简单工厂的问题在于:每加一种新产品,就得改工厂代码。违反了开闭原则。不过小项目用用还行,别太较真。
3.2 工厂方法
把工厂抽象成接口,每个产品对应一个具体工厂。这样加新产品时不用改已有代码,只要加一个新工厂类就行。
class Factory {
public:
virtual std::unique_ptr<Product> create() = 0;
virtual ~Factory() = default;
};
class FactoryA : public Factory {
public:
std::unique_ptr<Product> create() override {
return std::make_unique<ProductA>();
}
};
class FactoryB : public Factory {
public:
std::unique_ptr<Product> create() override {
return std::make_unique<ProductB>();
}
};
我的经验:工厂方法适合产品种类多、经常扩展的场景。但别过度设计——如果就两三个产品,简单工厂完全够用。
3.3 抽象工厂
工厂方法生产一种产品,抽象工厂生产一族产品。比如 GUI 库:Windows 风格有一整套按钮、菜单、文本框;Mac 风格也有一整套。抽象工厂就是管这个的。
抽象工厂的复杂度比较高,我一般只在跨平台库或者插件系统里用。平时写业务代码,工厂方法就够用了。
四、建造者模式(Builder)
4.1 什么时候用?
当一个对象的构造参数特别多,或者构造过程分多个步骤时,建造者模式就派上用场了。
我记得有个项目要构造一个复杂的网络请求对象:URL、headers、body、超时时间、重试策略……如果用构造函数传参,那签名得写十几行。用建造者模式就清爽多了。
4.2 C++ 实现示例
class HttpRequest {
public:
class Builder {
public:
Builder& setUrl(const std::string& url) {
url_ = url;
return *this;
}
Builder& setTimeout(int ms) {
timeout_ = ms;
return *this;
}
Builder& addHeader(const std::string& key, const std::string& value) {
headers_[key] = value;
return *this;
}
HttpRequest build() {
return HttpRequest(url_, timeout_, headers_);
}
private:
std::string url_;
int timeout_ = 3000;
std::map<std::string, std::string> headers_;
};
// 构造函数私有,只能通过 Builder 构造
private:
HttpRequest(const std::string& url, int timeout,
const std::map<std::string, std::string>& headers)
: url_(url), timeout_(timeout), headers_(headers) {}
// ...
};
用法:
auto req = HttpRequest::Builder()
.setUrl("https://api.example.com")
.setTimeout(5000)
.addHeader("Authorization", "Bearer xxx")
.build();
小技巧:Builder 的方法返回引用,实现链式调用。这是 C++ 里很常见的写法,看着舒服,用着也顺手。
五、原型模式(Prototype)
5.1 核心思想
通过拷贝已有对象来创建新对象,而不是通过 new。适合对象创建成本高、或者需要保留对象状态的情况。
我曾在游戏开发中用过原型模式:每个怪物类型只有一个原型对象,要生成怪物时就 clone 一份。这样不用每次都从配置文件读取数据,性能好很多。
5.2 C++ 实现
class Monster {
public:
virtual std::unique_ptr<Monster> clone() = 0;
virtual void attack() = 0;
virtual ~Monster() = default;
};
class Dragon : public Monster {
public:
std::unique_ptr<Monster> clone() override {
return std::make_unique<Dragon>(*this);
}
void attack() override {
std::cout << "Dragon breathes fire!\n";
}
};
注意:clone 函数里用的是拷贝构造函数。如果你的类里有指针成员,记得实现深拷贝,不然两个对象共享同一块内存,析构时 double free 就麻烦了。
六、知识体系总览
下面这张图把四种创建型模式的核心逻辑串起来了,方便你对照理解:
七、C++ 实现要点总结
| 模式 | 核心要点 | 常见坑 |
|---|---|---|
| 单例 | 构造函数私有、static 局部变量、delete 拷贝 | 线程安全(C++11 后局部 static 已安全)、析构顺序 |
| 工厂 | 虚函数、智能指针返回、开闭原则 | 过度抽象、工厂类爆炸 |
| 建造者 | 链式调用、Builder 内部类、build() 返回最终对象 | Builder 状态管理、参数校验时机 |
| 原型 | clone() 虚函数、拷贝构造函数 | 深拷贝 vs 浅拷贝、指针成员处理 |
最后说一句:设计模式是工具,不是目的。我见过有人为了用模式而用模式,把简单问题搞复杂了。我的建议是:先写出能跑的代码,再考虑要不要用模式重构。别一开始就套模式,那样反而束手束脚。