20、STL设计模式:迭代器模式、适配器模式、策略模式、观察者模式

设计模式这东西,很多人觉得是Java的专利。其实C++里,尤其是STL,早就把几种核心模式玩得炉火纯青了。我个人习惯把STL看作一本「设计模式活教材」——你天天用vector、sort、function,背后全是模式在撑腰。

今天咱们就聊四个:迭代器、适配器、策略、观察者。嗯,都是STL里高频出现的。你想想看,如果你能看透这些模式,写出来的代码会干净很多。

迭代器模式:STL的血管

迭代器模式,说白了就是「统一遍历接口」。不管底层是数组、链表还是红黑树,你给我一套begin()/end(),我就能用for循环扫过去。

我在项目中遇到过最典型的场景:业务方给了个自定义容器,底层是跳表。我问他「支持迭代器吗?」他说「啥是迭代器?」……后来我帮他封装了一层迭代器,所有算法直接复用,省了至少200行重复代码。

核心要点:迭代器把「遍历」和「容器」解耦。你不需要知道容器内部怎么存,只要知道怎么++、怎么*。
// 标准迭代器用法
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << " ";
}

// 自己实现一个简易迭代器
class MyContainer {
    int data[10];
public:
    using iterator = int*;
    iterator begin() { return data; }
    iterator end()   { return data + 10; }
};

迭代器有五种分类:输入、输出、前向、双向、随机访问。STL算法会根据迭代器类型自动选择最优实现。比如sort要求随机访问迭代器,list就不行——因为它只支持双向。

避坑指南:我曾经在for循环里用vec.end()每次重新计算,导致性能下降。后来改成auto end = vec.end(),速度翻倍。迭代器失效也是大坑——插入/删除后,之前的迭代器可能就废了。

适配器模式:让不兼容的接口握手

适配器模式,说白了就是「加一层包装」。你有一个接口A,我需要接口B,中间加个转换层就行。

STL里最典型的适配器就是stack、queue、priority_queue。它们底层默认用deque,但对外只暴露push/pop/top。你想想看,deque有20多个方法,stack只给你4个——这就是适配器在干活。

// stack 适配 deque
std::stack<int, std::deque<int>> stk;
stk.push(10);
stk.push(20);
std::cout << stk.top();  // 20

// 你也可以适配 vector
std::stack<int, std::vector<int>> stk2;
stk2.push(30);

还有迭代器适配器,比如reverse_iterator。你传一个正向迭代器,它给你反向遍历。底层就是包装了一下operator++变成operator--。

个人经验:我做过一个日志系统,需要把不同格式的日志输出到不同目标(文件、网络、控制台)。用适配器模式,每个输出目标写一个适配器,上层代码完全不用改。STL的适配器思想帮了大忙。

策略模式:算法的可插拔

策略模式,说白了就是「把算法当参数传进去」。STL里最典型的就是sort的第三个参数——比较器。

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

// 默认升序
std::sort(v.begin(), v.end());

// 策略:降序
std::sort(v.begin(), v.end(), std::greater<int>());

// 策略:自定义lambda
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a % 10 < b % 10;  // 按个位数排序
});

你想想看,sort本身不关心你怎么比大小,它只负责「排序」这个流程。比较策略是外挂的。这就是策略模式的核心——把变化的部分封装起来,通过组合代替继承。

避坑指南:我曾经在项目里用函数指针做策略,结果每次调用都有间接跳转开销。后来改成模板参数(比如std::sort的模板版本),编译器直接内联,性能提升明显。策略模式虽好,但要注意运行时多态的成本。

STL里还有std::function,它可以存储任意可调用对象。配合策略模式,你可以实现「运行时切换算法」。

// 运行时策略切换
std::function<bool(int, int)> comp;
if (need_ascending) {
    comp = std::less<int>();
} else {
    comp = std::greater<int>();
}
std::sort(v.begin(), v.end(), comp);

观察者模式:事件驱动的基石

观察者模式,说白了就是「发布-订阅」。一个对象状态变了,通知所有依赖它的对象。STL里没有直接实现观察者模式,但它的组件经常被用来搭建观察者系统。

我记得有一次做实时监控系统,需要监控多个数据源,数据变了要通知UI、日志、报警三个模块。用观察者模式,数据源是Subject,三个模块是Observer,代码结构非常清晰。

// 简易观察者模式(STL风格)
class Observer {
public:
    virtual void update(int value) = 0;
    virtual ~Observer() = default;
};

class Subject {
    std::vector<std::weak_ptr<Observer>> observers;
public:
    void attach(std::shared_ptr<Observer> obs) {
        observers.push_back(obs);
    }
    void notify(int value) {
        for (auto& wobs : observers) {
            if (auto obs = wobs.lock()) {
                obs->update(value);
            }
        }
    }
};
注意:观察者模式容易造成内存泄漏——如果Subject持有Observer的shared_ptr,两者互相引用就死锁了。我建议用weak_ptr,或者用std::function+回调的方式,更轻量。

STL里的std::signal和std::slot(C++11之前)也是观察者模式的体现。不过现代C++更推荐用std::function+回调列表来实现。

四种模式的关系

这四种模式不是孤立的。你想想看:

  • 迭代器模式让容器和算法解耦
  • 适配器模式让接口转换
  • 策略模式让算法可替换
  • 观察者模式让对象间通信解耦

它们共同的目标就是「高内聚、低耦合」。STL之所以强大,就是因为这些模式用得恰到好处。

我的建议:不要为了用模式而用模式。先写代码,等发现重复、耦合、难扩展的时候,再回头看这些模式——你会发现,STL早就给你铺好路了。
STL 四大设计模式关系图 迭代器模式 统一遍历接口 begin() / end() 适配器模式 接口转换 stack / queue 策略模式 算法可插拔 sort比较器 观察者模式 发布-订阅 回调 / signal 共同目标:高内聚、低耦合,让代码更灵活、可复用
总结一下:迭代器让你遍历无感,适配器让你接口兼容,策略让你算法灵活,观察者让你通信解耦。这四个模式,STL里天天在用。你写代码的时候,多想想「STL会怎么设计」——答案往往就在这些模式里。
公众号:蓝海资料掘金营,微信deep3321