先说一个现象:很多人学C++学到STL的时候,对std::stack和std::queue的用法倒背如流,push、pop、top、size张口就来,但一旦被人问起“这两个容器为什么没有rbegin()和rend()”或者“反向迭代器到底是怎么实现出来的”,当场就会卡壳。这节加餐课正好把这两件事放在一起讲清楚——stack_queue这类容器适配器的底层设计,以及反向迭代器(reverse_iterator)这个看上去不起眼、实际上把很多人绕晕的迭代器适配器。搞懂这两个点,你再看STL源码里的很多设计,都会有种“原来如此”的感觉。这篇文章适合已经能熟练使用STL、但想进一步理解容器底层结构和迭代器原理的读者,也适合正在准备C/C++方向面试的人,因为这两个知识点在面试里出现的频率比你想的高得多。
1. 先把stack_queue的底裤扒干净:容器适配器到底适配了什么
1.1 为什么说stack不是容器,而是“集装箱规则”
很多人第一次听到“stack是容器适配器(container adapter)”这个说法时都是一头雾水:我明明在用std::stack<int>,这不就是个容器吗?为什么说它不是容器?
关键在于“适配器”这个词。容器适配器本身并不持有数据,它只是把一个已经存在的底层容器包装起来,对外只暴露一组受限制的接口。类比一下就是:std::deque<int>是一堆货物,std::stack<int>是集装箱,集装箱规定了货物的进出顺序只能是“后进先出”,但货物本身还是放在原来的仓库里。
从<stack>头文件里扒出来的实现大概是这样的:
cpp复制template <class T, class Container = std::deque<T>>
class stack {
protected:
Container c; // 底层容器,数据全存在这里
public:
bool empty() const { return c.empty(); }
size_type size() const { return c.size(); }
reference top() { return c.back(); }
void push(const value_type& x) { c.push_back(x); }
void pop() { c.pop_back(); }
};
看到没有,stack自己几乎没有任何数据成员,所有数据都存放在那个Container c里面。stack做的只是在front/back/push_front/push_back这一堆底层容器的通用操作里,挑出back、push_back、pop_back这几个来用,并且改个更贴切的名字:top、push、pop。这就是适配器模式:改变接口,但不改变底层能力。
1.2 为什么默认底层是deque,而不是vector或者list
这是我在带新人时被问得最多的问题之一。std::stack默认底层是std::deque,但std::vector明明也能提供push_back、pop_back和back操作,为什么不用vector?
答案有三个层面:
第一,deque的头尾插入删除都是常数时间。vector只有在尾部插入删除是均摊O(1),如果在头部操作就是O(n)。虽然stack只操作尾部,看起来vector够用了,但deque还有一个优势是它的头部操作也是O(1),这让deque在“既要当stack用,又要当queue用”的场景下更灵活。
第二,deque的存储结构与vector不同。vector是一块连续内存,满容量时扩容需要搬移大量元素;deque是分段连续的,通过中控器(map)管理多个缓冲块,扩容时不需要搬移已有元素,代价是内存占用略高、迭代器结构更复杂。
第三,从标准委员会的角度看,选deque而不是vector作为默认,是为了同时兼顾stack和queue。如果stack默认用vector、queue默认用list,接口虽然都能跑通,但性能特征和内存行为会很不一致,使用者还得额外关注底层类型差异。
注意一个细节:std::queue的默认底层也是deque。为什么queue不用vector?因为queue需要pop_front,也就是从头部删除元素。vector头删是O(n),deque头删是O(1),所以vector当不了queue的底层。而std::priority_queue的默认底层是vector,因为它需要随机访问来配合堆算法(push_heap/pop_heap)。
1.3 底层容器可以换,换完之后行为差异很大
模板参数Container是可以指定的,这一点很多初学者不知道。比如:
cpp复制std::stack<int, std::vector<int>> s1;
std::stack<int, std::deque<int>> s2;
std::stack<int, std::list<int>> s3;
std::queue<int, std::list<int>> q1;
std::queue<int, std::deque<int>> q2;
换个底层容器,不只是性能变了,连迭代器失效规则都会变。vector的尾部插入可能会导致迭代器失效(扩容时全部失效),而list的插入永远不失效任何已有迭代器。如果你在写代码时混用了底层容器的迭代器
