26. 设计模式在C++标准库中的应用:STL迭代器、算法、函数对象中的模式体现

说实话,很多人在学设计模式时,总觉得那是Java或C#的专属。其实C++标准库本身就是一本活生生的设计模式教科书。我当年刚接触STL时,只觉得它好用,后来慢慢琢磨才发现——哦,原来迭代器模式、策略模式、命令模式,全藏在里面。

今天我们就来拆解一下,STL里到底藏了哪些设计模式。你想想看,每天用的vector、sort、lambda,背后都是经典模式的实践。

迭代器模式:STL的灵魂

迭代器模式,说白了就是提供一种统一的方式去遍历容器,而不暴露容器的内部结构。STL的迭代器就是最典型的例子。

核心思想: 容器和算法解耦。算法只依赖迭代器接口,不关心具体容器。

// 迭代器模式在STL中的体现
std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<int> lst = {10, 20, 30};

// 同样的算法,不同的容器
auto it1 = std::find(vec.begin(), vec.end(), 3);
auto it2 = std::find(lst.begin(), lst.end(), 20);

这里find算法根本不关心你传进来的是vector还是list的迭代器。它只要求迭代器支持解引用、自增、比较等操作。这就是迭代器模式的核心——抽象遍历行为。

我在项目中遇到过一个问题:有人自己写了一个自定义容器,但忘了提供迭代器接口。结果所有STL算法都用不了,只能自己手写循环。嗯,这就是没理解迭代器模式的代价。

策略模式:STL算法中的函数对象

策略模式允许你在运行时选择算法。STL的很多算法都接受一个可调用对象(函数指针、函数对象、lambda)作为最后一个参数,这就是策略模式的体现。

// 策略模式:通过函数对象定制排序策略
struct Descending {
    bool operator()(int a, int b) const {
        return a > b;
    }
};

std::vector<int> data = {3, 1, 4, 1, 5, 9};

// 默认策略:升序
std::sort(data.begin(), data.end());

// 自定义策略:降序
std::sort(data.begin(), data.end(), Descending());

// 或者用lambda,更灵活
std::sort(data.begin(), data.end(), 
          [](int a, int b) { return a % 10 < b % 10; });

你看,sort算法本身不关心排序规则,它只负责执行比较操作。比较策略由调用者传入。这就是策略模式——算法与策略分离。

我的习惯: 在写通用算法时,尽量让最后一个参数是可选的策略参数。这样既保留了默认行为,又给了调用者定制空间。

命令模式:std::function与回调

命令模式将请求封装为对象,从而支持参数化、队列化、日志化等操作。STL中的std::function就是命令模式的典型实现。

// 命令模式:将操作封装为可传递的对象
std::function<int(int, int)> cmd;

cmd = std::plus<int>{};      // 加法命令
std::cout << cmd(3, 4);      // 输出7

cmd = std::multiplies<int>{}; // 乘法命令
std::cout << cmd(3, 4);      // 输出12

// 甚至可以存储lambda
cmd = [](int a, int b) { return a * a + b * b; };
std::cout << cmd(3, 4);      // 输出25

我曾经在做一个任务调度系统时,就用std::function存储了各种任务命令。用户只需要注册一个函数对象,调度器统一执行。这就是命令模式在实战中的威力。

适配器模式:容器适配器与迭代器适配器

适配器模式将一个接口转换成客户希望的另一个接口。STL里到处都是适配器。

适配器类型 例子 说明
容器适配器 std::stack, std::queue, std::priority_queue 基于deque或vector提供受限接口
迭代器适配器 std::reverse_iterator, std::back_insert_iterator 改变迭代器的遍历方向或行为
函数适配器 std::bind, std::not_fn 调整函数对象的参数或逻辑
// 适配器模式:reverse_iterator
std::vector<int> v = {1, 2, 3, 4, 5};

// 反向遍历,不需要修改容器
for (auto it = v.rbegin(); it != v.rend(); ++it) {
    std::cout << *it << " ";  // 输出5 4 3 2 1
}

// back_inserter适配器:将push_back包装成迭代器赋值
std::vector<int> dest;
std::copy(v.begin(), v.end(), std::back_inserter(dest));

注意看,back_inserter把push_back操作伪装成了一个迭代器。算法只需要做赋值操作,背后实际执行的是插入。这就是适配器模式的精髓——接口转换。

工厂模式:std::make_shared与std::make_unique

虽然工厂模式在STL中不算最显眼,但std::make_shared和std::make_unique就是简单工厂的体现。它们封装了对象的创建过程,统一管理内存分配。

// 工厂模式:统一的对象创建接口
auto ptr = std::make_shared<MyClass>(arg1, arg2);
// 等价于:
// std::shared_ptr<MyClass> ptr(new MyClass(arg1, arg2));
// 但make_shared更安全、更高效

我曾经踩过的坑: 直接使用new创建shared_ptr,然后在构造函数中抛出异常,导致内存泄漏。用make_shared就能避免这个问题,因为它将对象创建和引用计数管理合并为一次内存分配。

观察者模式:std::signal与std::function回调

严格来说,STL没有直接实现观察者模式。但std::function加上std::vector就可以轻松构建一个观察者系统。很多C++框架就是这么做的。

// 简单的观察者模式实现
class Subject {
    std::vector<std::function<void(int)>> observers;
public:
    void attach(std::function<void(int)> obs) {
        observers.push_back(obs);
    }
    void notify(int value) {
        for (auto& obs : observers) {
            obs(value);  // 通知所有观察者
        }
    }
};

你看,这其实就是命令模式和观察者模式的结合。每个观察者都是一个命令对象,被统一存储和调用。

知识体系总览

下面这张图总结了STL中设计模式的分布情况,方便你快速回顾:

C++ STL 设计模式 迭代器模式 begin/end, 遍历抽象 策略模式 sort, find_if, 函数对象 命令模式 std::function, 回调封装 适配器模式 stack, reverse_iterator 工厂模式 make_shared, make_unique 观察者模式 function + vector 回调 核心思想:STL通过设计模式实现了算法与数据结构的完全解耦 迭代器 = 遍历抽象 | 函数对象 = 策略注入 | 适配器 = 接口转换 你写的每一个std::sort背后,都是设计模式的优雅实践

总结

STL的设计者并没有刻意去套用设计模式,但他们遵循了同样的原则——高内聚、低耦合、面向接口编程。你想想看,迭代器让算法和容器解耦,函数对象让算法和策略解耦,适配器让接口和实现解耦。

我个人建议,在写C++代码时,多想想STL是怎么设计的。它已经帮你验证了哪些模式是有效的。与其自己造轮子,不如站在STL的肩膀上。

一句话记住: STL不是设计模式的教科书,但它是最好的设计模式实践者。读懂STL,你就读懂了设计模式的精髓。

专注资料整理