单例模式
单例模式就是保证一个类仅有一个实例(对象),并且提供一个访问它的全局访问点。
-
使用场景
- 当你需要控制某些共享资源,或者需要一个全局唯一的管理者时。
- 日志打印
- 配置文件管理器
- 当你需要控制某些共享资源,或者需要一个全局唯一的管理者时。
-
实现条件
- 私有构造
- 禁止拷贝和赋值构造
- 提供一个静态方法
-
实现方式
-
饿汉模式:在程序启动(
main函数执行之前),单例对象就已经被创建并初始化好了。-
优点:天生线程安全。因为在多线程还没建立起来之前,对象就已经存在了,所以绝对不会出现两个线程同时去创建对象的情况。获取速度快,因为直接返回现成的。
-
缺点:浪费资源。如果这个单例对象非常庞大(比如加载了几个G的字典文件),但你这次运行程序刚好没用到它,那它的创建就是纯纯的浪费内存和启动时间。
c++#include <iostream> class EagerSingleton { public: // 获取实例的接口,直接返回已经做好的那盘"菜" static EagerSingleton* getInstance() { return instance; } void doSomething() { std::cout << "饿汉在工作..." << std::endl; } // 禁用拷贝和赋值 EagerSingleton(const EagerSingleton&) = delete; EagerSingleton& operator=(const EagerSingleton&) = delete; private: // 构造函数私有化 EagerSingleton() { std::cout << "-> 饿汉模式:程序还没进 main 函数,我就已经被创建了!" << std::endl; } // 声明一个静态指针(这是类级别的,不属于任何具体对象) static EagerSingleton* instance; }; // 【关键点】:在类外立即初始化静态变量! // 这一步发生在程序启动时,main() 函数执行之前。 EagerSingleton* EagerSingleton::instance = new EagerSingleton(); // ================= 测试代码 ================= int main() { std::cout << "--- 真正进入 main 函数了 ---" << std::endl; // 我们甚至还没开始用它,它就已经存在了 EagerSingleton* s1 = EagerSingleton::getInstance(); s1->doSomething(); return 0; } -
-
懒汉模式:对象在第一次被使用时 (也就是第一次调用
getInstance方法时)才会被动态创建。-
优点 :按需分配,节省资源。如果不使用,就永远不会被创建,不拖慢程序启动速度。
-
缺点 :存在线程安全隐患。如果两个线程同时(并发)第一次来喊吃饭,他可能脑子一抽,在厨房做了两份饭(创建了两个实例),这就破坏了单例规则。所以传统的懒汉模式需要加锁(性能开销),或者使用现代C++的局部静态变量(像上一个Demo那样)。
c++#include <iostream> #include <mutex> // 用于加锁保证线程安全 class LazySingleton { public: // 获取实例的接口:喊他吃饭了 static LazySingleton* getInstance() { // 【关键点】:先检查是不是还没做饭(是不是空指针) if (instance == nullptr) { // 传统懒汉在多线程下不安全,这里通常需要加锁 std::lock_guard<std::mutex> lck(_mutex); if (instance == nullptr) { // 双重检查锁定 (Double-Checked Locking) instance = new LazySingleton(); // 现做! } } return instance; } void doSomething() { std::cout << "懒汉在工作..." << std::endl; } // 禁用拷贝和赋值 LazySingleton(const LazySingleton&) = delete; LazySingleton& operator=(const LazySingleton&) = delete; private: LazySingleton() { std::cout << "-> 懒汉模式:你喊我了,我才刚被创建!" << std::endl; } static LazySingleton* instance; std::mutex _mutex; }; // 【关键点】:类外初始化为 nullptr(空指针),这时候不创建对象! LazySingleton* LazySingleton::instance = nullptr; // ================= 测试代码 ================= int main() { std::cout << "--- 真正进入 main 函数了 ---" << std::endl; std::cout << "程序运行了一会儿..." << std::endl; // 直到这里,懒汉单例都还不存在。 std::cout << "现在我要用它了:" << std::endl; LazySingleton* s1 = LazySingleton::getInstance(); // 此时才触发 new 操作 s1->doSomething(); return 0; }-
最经典、最安全、最推荐的:利用局部静态变量的初始化是天然线程安全的。
c++#include <iostream> #include <string> // 单例类:配置管理器 class ConfigManager { public: // 3. 提供一个全局静态方法,获取唯一实例 static ConfigManager& getInstance() { // 这里的局部静态变量 instance 在第一次调用时被初始化 // C++11标准保证了这里的初始化是线程安全的! static ConfigManager instance; return instance; } // --- 以下是业务方法 --- void setServerIP(const std::string& ip) { serverIP = ip; } std::string getServerIP() const { return serverIP; } // 2. 禁用拷贝构造函数和赋值操作符(防止克隆!) ConfigManager(const ConfigManager&) = delete; ConfigManager& operator=(const ConfigManager&) = delete; private: // 1. 构造函数私有化(外部无法 new) ConfigManager() { std::cout << "ConfigManager 实例被创建了!(这行字应该只会出现一次)" << std::endl; serverIP = "127.0.0.1"; // 默认IP } // 析构函数也私有化或保持默认,防止被外部 delete ~ConfigManager() { std::cout << "ConfigManager 实例被销毁了!" << std::endl; } // 内部数据 std::string serverIP; }; // ================= 测试代码 ================= int main() { std::cout << "--- 程序开始 ---" << std::endl; // 错误示范1:外部尝试直接创建会报错(编译不通过) // ConfigManager config; // ❌ 错误:构造函数是私有的 // ConfigManager* pConfig = new ConfigManager(); // ❌ 错误:构造函数是私有的 // 正确示范:通过 getInstance() 获取实例 std::cout << "第一次获取实例:" << std::endl; ConfigManager& config1 = ConfigManager::getInstance(); std::cout << "默认 IP: " << config1.getServerIP() << std::endl; // 修改配置 std::cout << "\n修改 IP 为 192.168.1.100..." << std::endl; config1.setServerIP("192.168.1.100"); // 第二次获取实例 std::cout << "\n第二次获取实例:" << std::endl; ConfigManager& config2 = ConfigManager::getInstance(); // 验证:看看 config2 里的 IP 是不是刚才 config1 修改后的 std::cout << "当前 IP: " << config2.getServerIP() << std::endl; // 验证:看看它们的内存地址是否完全一样 std::cout << "\nconfig1 的内存地址: " << &config1 << std::endl; std::cout << "config2 的内存地址: " << &config2 << std::endl; if (&config1 == &config2) { std::cout << "-> 结论:它们是同一个实例!单例模式生效。" << std::endl; } // 错误示范2:尝试克隆会报错(编译不通过) // ConfigManager config3 = config1; // ❌ 错误:拷贝构造函数已被 delete // ConfigManager config4; // config4 = config1; // ❌ 错误:赋值操作符已被 delete std::cout << "--- 程序结束 ---" << std::endl; return 0; }
-
-
工厂模式
把"对象的创建"和"对象的使用"分离开来,通俗解释:你直接找到一家"代工厂",对厂长说:"给我来一部 iPhone"。厂长负责在后台造好,直接把成品手机递给你。你根本不需要知道手机是怎么造出来的。
-
简单工厂
这是最基础的形态。整个系统里只有一个万能的工厂 ,你给它传一个参数(比如名字或枚举),它通过内部的
if-else或switch语句来决定造什么产品给你。-
优点:简单粗暴,小白也能看懂。
-
缺点 :不符合"开闭原则(Open-Closed Principle)"。每次你想增加一个新品牌(比如华为),你都必须扒开这个万能工厂内部的代码,强行往里面加一个
else if。
c++#include <iostream> #include <string> #include <memory> // 1. 定义产品的抽象接口(所有手机必须实现的功能) class Phone { public: virtual ~Phone() = default; virtual void call() = 0; // 纯虚函数 }; // 2. 具体产品类 class IPhone : public Phone { public: void call() override { std::cout << "用 iPhone 打电话:Siri 帮我拨号..." << std::endl; } }; class XiaomiPhone : public Phone { public: void call() override { std::cout << "用 小米 打电话:小爱同学 帮我拨号..." << std::endl; } }; // 3. 万能的简单工厂 class SimpleFactory { public: // 根据传入的类型,返回具体的智能指针对象 static std::unique_ptr<Phone> createPhone(const std::string& type) { if (type == "Apple") { return std::make_unique<IPhone>(); } else if (type == "Xiaomi") { return std::make_unique<XiaomiPhone>(); } return nullptr; } }; // ================= 测试代码 ================= int main() { std::cout << "--- 简单工厂演示 ---" << std::endl; // 客户端完全不需要 new IPhone(),直接找工厂要 auto myPhone1 = SimpleFactory::createPhone("Apple"); if (myPhone1) myPhone1->call(); auto myPhone2 = SimpleFactory::createPhone("Xiaomi"); if (myPhone2) myPhone2->call(); return 0; } -
-
工厂方法
为了解决简单工厂"每次加新产品都要修改万能工厂代码"的痛点,工厂方法模式诞生了。 核心思想 :把工厂也抽象化!我们不再用一个万能工厂,而是为每一种产品配备一个专属的工厂。
-
打个比方:现在我们有"苹果专属代工厂"和"小米专属代工厂"。你想买苹果,就去找苹果代工厂;想买小米,就去找小米代工厂。
-
优点 :完美符合"开闭原则"。以后要是想加"华为手机",你只需要写一个
HuaweiPhone类和一个HuaweiFactory类就行了,原有的任何代码都不用修改! -
缺点:类太多了。每加一个产品,就要配套加一个工厂类,容易造成"类爆炸"。
c++// 1. 抽象工厂接口(定义所有专属工厂必须具备的造手机能力) class AbstractPhoneFactory { public: virtual ~AbstractPhoneFactory() = default; virtual std::unique_ptr<Phone> createPhone() = 0; }; // 2. 具体的专属工厂 class AppleFactory : public AbstractPhoneFactory { public: std::unique_ptr<Phone> createPhone() override { return std::make_unique<IPhone>(); } }; class XiaomiFactory : public AbstractPhoneFactory { public: std::unique_ptr<Phone> createPhone() override { return std::make_unique<XiaomiPhone>(); } }; // ================= 测试代码 ================= int main() { std::cout << "--- 工厂方法演示 ---" << std::endl; // 我想要苹果手机,所以我先找到苹果代工厂 std::unique_ptr<AbstractPhoneFactory> appleFactory = std::make_unique<AppleFactory>(); auto myPhone = appleFactory->createPhone(); myPhone->call(); // 如果后续想造华为,完全不用改上面的代码,自己扩展即可 return 0; } -
-
抽象工厂模式
这是工厂模式的最终形态。它解决的是**"产品族(一系列相关的产品)"的问题。 核心思想:刚才的工厂只能造 单一产品**(手机)。但现实中,苹果代工厂不仅造 iPhone,还造 MacBook(电脑);小米不仅造手机,还造小米笔记本。为了保证买到的手机和电脑是同一个生态/同一家厂的,我们需要抽象工厂。
-
打个比方:抽象工厂是一个大型综合代工厂的图纸。它规定了这个厂必须能同时造"手机"和"电脑"。苹果厂和小米厂按照这个图纸各自建厂。
-
优点:保证客户端始终只使用同一个产品族中的对象(用苹果手机就配苹果电脑,不会串味)。
-
缺点 :很难增加新的产品种类。比如你突然要求工厂不仅造手机和电脑,还要造"智能手表",那你得把所有的接口和所有的具体工厂类全修改一遍。
c++#include <iostream> #include <memory> // --- 第一部分:定义所有的产品接口 --- class Phone { public: virtual ~Phone() = default; virtual void call() = 0; }; class Laptop { // 新增产品:电脑 public: virtual ~Laptop() = default; virtual void code() = 0; }; // --- 第二部分:定义具体的产品 --- class IPhone : public Phone { public: void call() override { std::cout << "iPhone 拨号中..." << std::endl; } }; class MacBook : public Laptop { public: void code() override { std::cout << "MacBook 写C++代码中..." << std::endl; } }; class XiaomiPhone : public Phone { public: void call() override { std::cout << "小米手机 拨号中..." << std::endl; } }; class MiLaptop : public Laptop { public: void code() override { std::cout << "小米电脑 写C++代码中..." << std::endl; } }; // --- 第三部分:定义抽象工厂(规定必须能造手机和电脑) --- class DigitalFactory { public: virtual ~DigitalFactory() = default; virtual std::unique_ptr<Phone> createPhone() = 0; virtual std::unique_ptr<Laptop> createLaptop() = 0; }; // --- 第四部分:具体的品牌工厂(实现产品族) --- class AppleDigitalFactory : public DigitalFactory { public: std::unique_ptr<Phone> createPhone() override { return std::make_unique<IPhone>(); } std::unique_ptr<Laptop> createLaptop() override { return std::make_unique<MacBook>(); } }; class XiaomiDigitalFactory : public DigitalFactory { public: std::unique_ptr<Phone> createPhone() override { return std::make_unique<XiaomiPhone>(); } std::unique_ptr<Laptop> createLaptop() override { return std::make_unique<MiLaptop>(); } }; // ================= 测试代码 ================= int main() { std::cout << "--- 抽象工厂演示 ---" << std::endl; // 客户说:我要一套苹果全家桶! std::unique_ptr<DigitalFactory> factory = std::make_unique<AppleDigitalFactory>(); // 工厂开始统一生产苹果生态的产品 auto myPhone = factory->createPhone(); auto myLaptop = factory->createLaptop(); myPhone->call(); myLaptop->code(); return 0; } -
建造者模式
允许你一步一步地定制出一个极其复杂的专属产品
-
为什么需要
假设我们要写一个
Computer类,一台电脑有非常多的配件:CPU、主板、内存、硬盘、显卡、电源、机箱、散热器...... 如果你用传统的构造函数来创建对象,你的代码会变成这样:c++// 构造函数参数爆炸(这被称为"伸缩构造函数反模式") Computer myPC = Computer("i9-13900K", "Z790", "32G", "1TB", "RTX4090", "1000W", "海景房机箱", "水冷");- 参数太多了!稍微写错一个位置(比如把内存和硬盘传反了),编译器也查不出来,运行时直接崩溃。
- 很多配件是可选的 。比如有些办公机不需要独立显卡,有些不需要水冷。为了应付这些情况,你得写无数个重载的构造函数,或者在参数里狂塞
nullptr或"",代码极度丑陋。
-
有了建造者模式后(现代做法):
-
我们引入一个"装机老板(Builder)"。你不需要一次性把所有参数报出来,而是像点菜一样,一步步跟老板说你要什么。老板把你的需求记在小本本上,最后你喊一句"装机!",老板就把配置好的电脑交给你。
c++#include <iostream> #include <string> #include <memory> // --- 1. 复杂的产品类:Computer --- class Computer { public: // 允许 Builder 访问私有成员(或者把构造函数设为 public,看具体需求,这里用友元更严谨) friend class ComputerBuilder; void showConfiguration() const { std::cout << "=== 您的电脑配置清单 ===" << std::endl; std::cout << "CPU: " << cpu << std::endl; std::cout << "内存: " << ram << std::endl; std::cout << "存储: " << storage << std::endl; // 可选配件 if (!gpu.empty()) std::cout << "显卡: " << gpu << std::endl; if (!cooler.empty()) std::cout << "散热: " << cooler << std::endl; std::cout << "========================" << std::endl; } private: // 构造函数私有化,强制要求通过 Builder 来创建 Computer() = default; // 必填和选填的属性 std::string cpu; std::string ram; std::string storage; std::string gpu; // 可选 std::string cooler; // 可选 }; // --- 2. 建造者类:ComputerBuilder --- class ComputerBuilder { public: ComputerBuilder() { // 在开始建造时,先准备一个空的产品骨架 computer = std::make_unique<Computer>(); } // --- 以下是各个配件的"定制步骤" --- // 【关键点】:返回引用 ComputerBuilder&,以便支持链式调用 ComputerBuilder& setCPU(const std::string& cpuModel) { computer->cpu = cpuModel; return *this; } ComputerBuilder& setRAM(const std::string& ramSize) { computer->ram = ramSize; return *this; } ComputerBuilder& setStorage(const std::string& storageSize) { computer->storage = storageSize; return *this; } ComputerBuilder& setGPU(const std::string& gpuModel) { computer->gpu = gpuModel; return *this; } ComputerBuilder& setCooler(const std::string& coolerType) { computer->cooler = coolerType; return *this; } // --- 3. 最终交付步骤:build() --- // 调用这个方法,表示组装完毕,把产品交出去 std::unique_ptr<Computer> build() { // 可以在这里加上校验逻辑,比如:"没有装CPU就不准出货!" if (computer->cpu.empty() || computer->ram.empty()) { throw std::runtime_error("组装失败:CPU和内存是必填项!"); } // 使用 std::move 把所有权转移给调用者 return std::move(computer); } private: std::unique_ptr<Computer> computer; }; // ================= 测试代码 ================= int main() { try { std::cout << "--- 客户 A:我要配一台顶级游戏主机! ---" << std::endl; // 链式调用,一气呵成,可读性极强!而且不在乎调用顺序 std::unique_ptr<Computer> gamingPC = ComputerBuilder() .setCPU("Intel i9-13900K") .setGPU("NVIDIA RTX 4090") // 先装显卡再装内存也没关系 .setRAM("64GB DDR5") .setStorage("4TB SSD") .setCooler("360水冷") .build(); // 最后点击组装 gamingPC->showConfiguration(); std::cout << "\n--- 客户 B:我只要一台简单的办公机,不要显卡 ---" << std::endl; // 缺省了 GPU 和 Cooler,也不会有几万个构造函数重载的问题 std::unique_ptr<Computer> officePC = ComputerBuilder() .setCPU("Intel i5-13400") .setRAM("16GB DDR4") .setStorage("512GB SSD") .build(); officePC->showConfiguration(); } catch (const std::exception& e) { std::cerr << "报错了: " << e.what() << std::endl; } return 0; } -
-
工厂模式 vs 建造者模式
| 特点 | 工厂模式 (Factory) | 建造者模式 (Builder) |
|---|---|---|
| 关注点 | "造什么"(不管过程,直接要成品)。 | "怎么造"(关注一步步组装的细节和个性化配置)。 |
| 现实比喻 | 去买一台组装好的品牌机(如 联想拯救者),不需要你知道里面有啥。 | 去电脑城DIY组装机,由你亲自决定挑哪款主板、哪款显卡。 |
| 代码表现 | 传一个参数进去,直接 return 一个完整的对象。 |
调用很多个 setXXX() 方法,最后调用 build() 才得到对象。 |
迭代器模式
迭代器模式就是提供一种方法,让你能够顺序访问一个"集合(比如数组、链表)"里面的各个元素,又不需要暴露这个集合底层的内部结构。
c++
#include <iostream>
#include <string>
// 1. 数据载体:书本
struct Book {
std::string name;
};
// 2. 自定义集合:书架
class Bookshelf {
public:
Bookshelf() : count(0) {}
// 添加书籍
void addBook(const std::string& name) {
if (count < 100) {
books[count].name = name;
count++;
}
}
// ==========================================
// 核心开始:内部定义一个迭代器类
// 它的作用就是充当一个"聪明的指针",用来游走于书架之间
// ==========================================
class Iterator {
public:
// 构造函数:接收一个指向当前书本的普通指针
Iterator(Book* ptr) : current_ptr(ptr) {}
// 必须重载的运算符 1:解引用 (*),获取当前指着的书本
Book& operator*() {
return *current_ptr;
}
// 必须重载的运算符 2:自增 (++),走到下一本书
Iterator& operator++() {
current_ptr++; // 指针往后挪一个位置
return *this;
}
// 必须重载的运算符 3:不等于 (!=),判断是否已经遍历到底了
bool operator!=(const Iterator& other) const {
return current_ptr != other.current_ptr;
}
private:
Book* current_ptr; // 迭代器内部藏着一个真正的游标(指针)
};
// ==========================================
// 为书架提供 begin() 和 end() 接口
// ==========================================
// 指向第一本书
Iterator begin() {
return Iterator(&books[0]);
}
// 指向最后一本书的【下一个位置】(表示越界/结束)
Iterator end() {
return Iterator(&books[count]);
}
private:
Book books[100]; // 底层数据结构:固定大小的普通数组
int count; // 当前存了多少本书
};
// ================= 测试代码 =================
int main() {
std::cout << "--- 迭代器模式演示 ---" << std::endl;
// 1. 创建一个书架并塞入几本书
Bookshelf myShelf;
myShelf.addBook("《C++ Primer》");
myShelf.addBook("《Effective C++》");
myShelf.addBook("《设计模式》");
std::cout << "\n方式一:使用我们自定义的迭代器手动遍历" << std::endl;
// 获取迭代器的起点和终点
Bookshelf::Iterator it = myShelf.begin();
Bookshelf::Iterator end = myShelf.end();
// 只要没到终点,就一直往后走
while (it != end) {
std::cout << "阅读: " << (*it).name << std::endl;
++it; // 按下"下一首"按钮
}
std::cout << "\n方式二:C++11 语法糖(范围 for 循环)" << std::endl;
// 【魔法时刻】:因为我们实现了 begin, end, *, ++, !=
// 编译器会自动把下面这行极度简洁的代码,翻译成上面"方式一"的复杂样子!
for (auto& book : myShelf) {
std::cout << "重温: " << book.name << std::endl;
}
return 0;
}
-
优点
- 信息隐藏 :客户端(
main函数)里,你根本看不到Book books[100]这个底层数组。以后哪天如果你嫌数组不好用,把底层的books换成了链表,main函数里的遍历代码连一个标点符号都不用改! 这就叫解耦。 - 职责分离 :
Bookshelf这个类只负责"怎么存书"(增删查),而Iterator这个类只负责"怎么翻书"。各司其职,符合面向对象的单一职责原则。 - 算法与数据的桥梁 :在 C++ 的 STL 中,你可以用同一个
std::sort算法,去排序vector,也可以去排序deque。凭什么?就因为它们都提供了标准的迭代器。迭代器把各种千奇百怪的数据结构,统一变成了同一种访问方式。
- 信息隐藏 :客户端(
适配器模式
当你希望复用一些现有的类(老代码、第三方库),但是它的接口(方法名、参数)跟你当前系统要求的接口不一致时,你就可以写一个适配器类(Adapter)。适配器负责把老接口"伪装"成新接口,让本来水火不容的两个类能够一起工作。
-
实现方式:C++ 中的两种适配器:对象适配器 vs 类适配器
-
对象适配器(组合模式)
- 做法 :适配器类继承新接口,并在内部**包含(组合)**一个老对象的指针。
- 优点:极其灵活,不仅能适配老类本身,还能适配老类的所有子类。符合"多用组合,少用继承"的金科玉律。
-
类适配器(多重继承)
- 做法 :适配器类同时多重继承新接口和老类(通常是 public 继承新接口,private 继承老类)。
- 缺点:C++ 独有的花活,因为 Java 等语言不支持多继承。这种方式耦合度太高,且无法适配老类的子类,现代 C++ 开发中已经很少用了。
c++#include <iostream> #include <memory> // ========================================== // 1. 目标接口 (Target):当前系统期待的新接口 // 也就是我们的新手机,它只认 Type-C // ========================================== class TypeCPhone { public: virtual ~TypeCPhone() = default; // 手机规定:听歌必须调用这个 Type-C 方法 virtual void listenMusicWithTypeC() = 0; }; // ========================================== // 2. 被适配者 (Adaptee):老代码 / 第三方库 // 也就是我们的 3.5mm 老耳机,功能完美,但接口不对 // ========================================== class Old35mmEarphone { public: // 老耳机的播放方法,名字和新手机要求的完全不同 void plugInto35mmHole() { std::cout << "-> 3.5mm 经典耳机已接入,开始播放无损高保真音乐!🎵" << std::endl; } }; // ========================================== // 3. 适配器 (Adapter):转接头登场! // 核心:实现新接口,内部藏着老对象 // ========================================== class EarphoneAdapter : public TypeCPhone { public: // 构造函数:把老耳机插到转接头里 EarphoneAdapter(std::shared_ptr<Old35mmEarphone> earphone) : oldEarphone(earphone) {} // 实现新系统要求的方法 void listenMusicWithTypeC() override { std::cout << "[转接头工作] 接收 Type-C 数字信号,转换为 3.5mm 模拟信号..." << std::endl; // 【关键魔法】:表面上调用的是新方法,实际上偷偷调用了老对象的老方法 if (oldEarphone) { oldEarphone->plugInto35mmHole(); } } private: // 内部"组合"了一个老对象的智能指针 std::shared_ptr<Old35mmEarphone> oldEarphone; }; // ================= 测试代码 ================= int main() { std::cout << "--- 适配器模式演示 ---" << std::endl; // 1. 翻出我们落灰的、但是音质极好的老耳机 auto myOldEarphone = std::make_shared<Old35mmEarphone>(); // 注意:这个时候如果你想直接把 myOldEarphone 插进手机是做不到的,因为类型不匹配 // 2. 买一个转接头,把老耳机插进去 // 返回值是一个 TypeCPhone 的指针,意味着它现在伪装成了一个支持 Type-C 的设备 std::unique_ptr<TypeCPhone> adapterDongle = std::make_unique<EarphoneAdapter>(myOldEarphone); // 3. 手机开机,开始听歌! // 手机(客户端)只管调用 Type-C 的方法,根本不知道背后其实是老耳机在出声 std::cout << "\n手机:准备播放音乐..." << std::endl; adapterDongle->listenMusicWithTypeC(); return 0; } -
观察者模式
观察者模式定义了对象之间的一对多依赖关系。当**一个对象(UP主/发布者)的状态发生改变时,所有依赖于它的对象(粉丝/订阅者)**都会得到通知并被自动更新。
-
为什么需要
- 没有观察者模式时(轮询): 你非常喜欢某位 UP 主,你急切地想看他的新视频。于是你每天定闹钟,每隔一小时就去刷新他的个人主页,看看有没有更新。
- 痛点:这种方式叫"轮询(Polling)"。你极度浪费时间(在代码中就是极度浪费 CPU 资源),而且 UP 主可能根本没更新。
- 有了观察者模式后(事件驱动):你点了一下"关注并开启小铃铛" 。然后你就可以去干别的事了。当 UP 主发视频的那一刻,系统会自动给你,以及其他几百万粉丝的手机上弹出一条推送:"你关注的 UP 主更新啦!"。
- 优点 :你不需要去盯着他了,状态一有变化,他会主动来通知你。
- 没有观察者模式时(轮询): 你非常喜欢某位 UP 主,你急切地想看他的新视频。于是你每天定闹钟,每隔一小时就去刷新他的个人主页,看看有没有更新。
-
实现
-
Subject(主题 / 发布者):也就是 UP 主。他手里必须攥着一个**"粉丝名单(列表)"**。他需要提供三个基本功能:
- 允许别人关注他(添加到名单)。
- 允许别人取关(从名单移除)。
- 群发通知(遍历名单,挨个通知)。
-
Observer(观察者 / 订阅者) :也就是粉丝。粉丝必须提供一个统一的接口,比如叫
update()。这样 UP 主才能通过这个统一的接口把消息传达给你。
c++#include <iostream> #include <string> #include <list> // ========================================== // 1. 观察者接口 (Observer):所有粉丝必须遵守的规范 // ========================================== class Observer { public: virtual ~Observer() = default; // 接收通知的统一方法 virtual void update(const std::string& videoTitle) = 0; }; // ========================================== // 2. 主题接口 (Subject):UP主必须具备的功能 // ========================================== class Subject { public: virtual ~Subject() = default; virtual void attach(Observer* observer) = 0; // 关注(加入粉丝群) virtual void detach(Observer* observer) = 0; // 取关(踢出粉丝群) virtual void notify(const std::string& videoTitle) = 0; // 发送群通知 }; // ========================================== // 3. 具体的观察者 (Concrete Observer):真实的粉丝 // ========================================== class BilibiliFan : public Observer { public: BilibiliFan(const std::string& name) : fanName(name) {} // 粉丝收到通知后的具体反应 void update(const std::string& videoTitle) override { std::cout << "[粉丝 " << fanName << " 收到推送]:哇!新视频《" << videoTitle << "》更新了,火速去一键三连!" << std::endl; } private: std::string fanName; }; // ========================================== // 4. 具体的主题 (Concrete Subject):真实的UP主 // ========================================== class UpHost : public Subject { public: // 粉丝点关注 void attach(Observer* observer) override { fansList.push_back(observer); } // 粉丝取消关注 void detach(Observer* observer) override { fansList.remove(observer); } // 内部方法:遍历粉丝列表,挨个调用他们的 update 方法 void notify(const std::string& videoTitle) override { for (auto* fan : fansList) { fan->update(videoTitle); } } // UP主的实际业务逻辑:发布视频 void publishVideo(const std::string& title) { std::cout << "\n【系统广播】UP主发布了全新视频:《" << title << "》" << std::endl; std::cout << "正在向所有粉丝发送推送通知..." << std::endl; // 视频发完,立刻触发通知机制 notify(title); } private: // 核心数据结构:用来存放所有关注了这个UP主的粉丝指针 std::list<Observer*> fansList; }; // ================= 测试代码 ================= int main() { std::cout << "--- 观察者模式演示 ---" << std::endl; // 1. 创建一个 UP 主 UpHost techUpHost; // 2. 创建三个粉丝 BilibiliFan fan1("张三"); BilibiliFan fan2("李四"); BilibiliFan fan3("王五"); // 3. 粉丝们开始关注 UP 主 techUpHost.attach(&fan1); techUpHost.attach(&fan2); techUpHost.attach(&fan3); // 4. UP 主第一次发视频 techUpHost.publishVideo("C++ 设计模式完全指南"); // 5. 李四觉得视频太硬核,取关了 std::cout << "\n[动态] 粉丝 李四 取消了关注。" << std::endl; techUpHost.detach(&fan2); // 6. UP 主第二次发视频 (此时李四应该收不到通知了) techUpHost.publishVideo("手撕红黑树源码"); return 0; } -
-
优点:解耦
- UP 主根本不在乎粉丝是谁 :他不需要知道张三是干嘛的,李四住在哪里。他只知道一件事:"只要在我的
fansList里,并且有update()方法的人,我就把字符串塞给他"。 - 动态的热插拔 :你可以在程序运行的任何时刻,动态地添加新的观察者,或者移除旧的观察者,而完全不需要修改 UP 主的核心代码。
- UP 主根本不在乎粉丝是谁 :他不需要知道张三是干嘛的,李四住在哪里。他只知道一件事:"只要在我的
策略模式
-
为什么要用
-
假设你要写一个地图导航软件,用户输入起点和终点后,系统要规划路线。 一开始,你只有"驾车"功能。后来产品经理说要加"步行",再后来又要加"骑行"、"公交"...... 如果你不改变代码结构,你的
Navigator(导航器)类里的buildRoute()方法会变成这样:c++void buildRoute(起点, 终点, 出行方式) { if (出行方式 == "驾车") { // 避开拥堵、计算油耗、考虑限行... (几百行代码) } else if (出行方式 == "步行") { // 寻找人行道、抄近道、避开高速... (几百行代码) } else if (出行方式 == "公交") { // 查找公交站、计算换乘、预估步行时间... (几百行代码) } }痛点:这个类会无限膨胀(几千行代码)。只要修改其中一种导航逻辑,就极容易不小心把其他逻辑改出 Bug。每次增加新出行方式,都要修改这个核心类,严重违背了**"开闭原则"**。
-
有了策略模式后:策略模式的思路是:把每一种"出行方式的算法"单独抽离出来,封装成独立的类(这叫"策略")。 导航软件本身(上下文)不再负责计算路线,它只负责把用户的起点和终点外包给具体的"策略类"去计算。
-
-
实现
-
分工
- Strategy(策略接口) :所有具体算法必须遵守的统一规范(比如都必须有一个
calculateRoute方法)。 - Concrete Strategy(具体策略) :实现了上面接口的实体类,比如
DriveStrategy(驾车)、WalkStrategy(步行)。 - Context(上下文):也就是使用策略的主体(导航软件)。它内部持有一个策略接口的指针,用户告诉它用哪个策略,它就去调用哪个。
- Strategy(策略接口) :所有具体算法必须遵守的统一规范(比如都必须有一个
c++#include <iostream> #include <string> #include <memory> // ========================================== // 1. 策略接口 (Strategy):定义所有出行算法的通用规范 // ========================================== class RouteStrategy { public: virtual ~RouteStrategy() = default; // 所有的策略都必须能规划路线 virtual void buildRoute(const std::string& start, const std::string& end) = 0; }; // ========================================== // 2. 具体策略 (Concrete Strategies):各种具体的出行算法 // ========================================== class DriveStrategy : public RouteStrategy { public: void buildRoute(const std::string& start, const std::string& end) override { std::cout << "[驾车路线] 从 " << start << " 到 " << end << " -> 走高速,距离 20km,预计 30 分钟,注意避让行人。" << std::endl; } }; class WalkStrategy : public RouteStrategy { public: void buildRoute(const std::string& start, const std::string& end) override { std::cout << "[步行路线] 从 " << start << " 到 " << end << " -> 走人行天桥和林荫小道,距离 2km,预计 25 分钟,就当锻炼身体。" << std::endl; } }; class PublicTransitStrategy : public RouteStrategy { public: void buildRoute(const std::string& start, const std::string& end) override { std::cout << "[公交路线] 从 " << start << " 到 " << end << " -> 乘坐地铁 2 号线转 4 号线,预计 40 分钟,车费 5 元。" << std::endl; } }; // ========================================== // 3. 上下文 (Context):导航软件 // 它本身不计算路线,它只负责调用当前的策略 // ========================================== class Navigator { public: // 动态设置/切换当前的出行策略 void setStrategy(std::unique_ptr<RouteStrategy> newStrategy) { strategy = std::move(newStrategy); } // 执行核心业务:规划路线 void executeRoutePlanning(const std::string& start, const std::string& end) { if (!strategy) { std::cout << "错误:请先选择出行方式(设置策略)!" << std::endl; return; } std::cout << "\n开始为您规划路线..." << std::endl; // 把具体工作委托给当前的策略对象 strategy->buildRoute(start, end); } private: // 内部持有一个策略接口的智能指针 std::unique_ptr<RouteStrategy> strategy; }; // ================= 测试代码 ================= int main() { std::cout << "--- 策略模式演示 ---" << std::endl; Navigator amap; // 打开高德地图 std::string startPoint = "家里"; std::string endPoint = "公司"; // 1. 今天下雨,决定开车 std::cout << "\n>>> 用户选择:驾车模式"; amap.setStrategy(std::make_unique<DriveStrategy>()); amap.executeRoutePlanning(startPoint, endPoint); // 2. 突然发现车牌今天限行,改坐地铁吧 std::cout << "\n>>> 用户切换:公交模式"; amap.setStrategy(std::make_unique<PublicTransitStrategy>()); amap.executeRoutePlanning(startPoint, endPoint); // 3. 下班了,天气不错,离家近,走回去 std::cout << "\n>>> 用户切换:步行模式"; amap.setStrategy(std::make_unique<WalkStrategy>()); amap.executeRoutePlanning(startPoint, endPoint); return 0; }- 补充:也可以通过回调函数的方式实现轻量的
-
-
优点
- 热插拔 :在程序运行过程中,导航软件(
Navigator)可以随意更换内部的算法引擎(通过setStrategy),而且不需要重启软件,也不需要重新实例化导航软件本身。 - 彻底解耦 :如果明天你想加一个"骑共享单车"的策略,你只需要新建一个
BikeStrategy类继承RouteStrategy就行了。Navigator类的代码哪怕一个标点符号都不用改! 这就是完美的面向对象设计。
- 热插拔 :在程序运行过程中,导航软件(
其他
- 代理模式
- 用途:为其他对象提供一种代理以控制对这个对象的访问。
- C++实现特点 :常用于延迟加载、权限控制或日志记录。实际上,现代C++中的智能指针(如
std::shared_ptr和std::unique_ptr)就是一种代理模式的应用,它们代理了裸指针,提供了自动的生命周期管理。
- 装饰器模式 (Decorator)
- 用途:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
- C++实现特点:通过组合来实现,装饰器类包含一个指向基类接口的指针(或智能指针),可以在调用原生方法前后注入新的行为。