在C++标准模板库(STL)中,stack和queue是两种最常用的容器适配器。它们之所以被称为"容器适配器",是因为它们并不是独立的容器,而是在现有容器基础上提供特定接口的封装。这就像给手机装上不同功能的保护壳——手机本身的功能不变,但通过不同的外壳可以提供不同的使用体验。
stack(栈)遵循LIFO(后进先出)原则,就像餐厅里叠放的盘子,你总是取走最上面的那个。queue(队列)则遵循FIFO(先进先出)原则,如同超市的收银台排队,先来的人先结账。理解这两种数据结构的实现原理,对于深入掌握C++ STL至关重要。
stack在STL中是通过封装底层容器实现的,默认使用deque(双端队列)作为底层容器。这种设计体现了软件工程中的"适配器模式"——不改变原有组件,只是改变其接口。以下是stack类的基本框架:
cpp复制template<class T, class Container = deque<T>>
class stack {
public:
// 接口函数
private:
Container _con; // 底层容器
};
选择deque作为默认容器有几个关键原因:
stack的接口实现本质上是对底层容器操作的转发:
cpp复制void push(const T& x) {
_con.push_back(x); // 在尾部插入元素
}
void pop() {
_con.pop_back(); // 删除尾部元素
}
const T& top() {
return _con.back(); // 返回尾部元素引用
}
size_t size() {
return _con.size(); // 返回元素数量
}
bool empty() {
return _con.empty();// 判断是否为空
}
注意:top()返回的是const引用,这防止了外部直接修改栈顶元素,保持了stack的封装性。如果需要修改栈顶元素,应该先pop()再push()新值。
虽然deque是默认选择,但stack也可以使用其他容器作为底层实现,只要该容器支持以下操作:
例如使用vector实现的stack:
cpp复制stack<int, vector<int>> vec_stack;
使用list实现的stack:
cpp复制stack<int, list<int>> list_stack;
不同底层容器的性能特点:
| 容器类型 | push/pop复杂度 | 内存分配策略 | 适用场景 |
|---|---|---|---|
| deque | O(1) | 分块连续存储 | 通用场景(默认) |
| vector | O(1)~O(n) | 连续存储,需扩容 | 需要连续内存时 |
| list | O(1) | 非连续存储 | 频繁插入删除 |
queue与stack类似,也是容器适配器,但它的行为完全不同。queue默认也使用deque作为底层容器,其基本框架如下:
cpp复制template<class T, class Container = deque<T>>
class queue {
public:
// 接口函数
private:
Container _con;
};
queue需要支持从一端插入,另一端删除,因此其接口实现如下:
cpp复制void push(const T& x) {
_con.push_back(x); // 尾部插入
}
void pop() {
_con.pop_front(); // 头部删除
}
const T& front() {
return _con.front();// 获取头部元素
}
const T& back() {
return _con.back(); // 获取尾部元素
}
size_t size() {
return _con.size(); // 元素数量
}
bool empty() {
return _con.empty();// 是否为空
}
关键区别:queue需要pop_front()操作,这意味着底层容器必须支持高效的头删操作。这也是为什么list也可以作为queue的底层容器,而vector不行。
有效的queue底层容器必须支持:
常见选择:
deque(默认):
list:
将stack和queue放在同一个命名空间中是个好习惯,可以避免名称冲突:
cpp复制namespace stk {
// stack实现...
// queue实现...
}
全面的测试应该包括正常情况和边界情况:
cpp复制void test_stack() {
stk::stack<int> s;
// 基本功能测试
s.push(1);
s.push(2);
assert(s.size() == 2);
assert(s.top() == 2);
s.pop();
assert(s.top() == 1);
// 边界测试
while(!s.empty()) s.pop();
assert(s.empty());
try {
s.pop(); // 应该处理空栈pop
} catch(...) {
cout << "Caught exception on empty pop\n";
}
}
void test_queue() {
stk::queue<string> q;
// 基本功能测试
q.push("first");
q.push("second");
assert(q.front() == "first");
assert(q.back() == "second");
q.pop();
assert(q.front() == "second");
// 边界测试
while(!q.empty()) q.pop();
assert(q.empty());
try {
q.pop(); // 应该处理空队列pop
} catch(...) {
cout << "Caught exception on empty pop\n";
}
}
比较不同底层容器的性能差异:
cpp复制void performance_test() {
const int N = 1000000;
// deque stack
auto start = chrono::high_resolution_clock::now();
stk::stack<int> s1;
for(int i=0; i<N; ++i) s1.push(i);
for(int i=0; i<N; ++i) s1.pop();
auto end = chrono::high_resolution_clock::now();
cout << "deque stack time: "
<< chrono::duration_cast<chrono::milliseconds>(end-start).count()
<< "ms\n";
// vector stack
start = chrono::high_resolution_clock::now();
stk::stack<int, vector<int>> s2;
for(int i=0; i<N; ++i) s2.push(i);
for(int i=0; i<N; ++i) s2.pop();
end = chrono::high_resolution_clock::now();
cout << "vector stack time: "
<< chrono::duration_cast<chrono::milliseconds>(end-start).count()
<< "ms\n";
}
良好的实现应该考虑异常安全性:
cpp复制void push(const T& x) {
try {
_con.push_back(x);
} catch(...) {
// 处理内存分配失败等情况
throw;
}
}
标准STL容器不是线程安全的,如果需要在多线程环境中使用,需要添加锁:
cpp复制template<typename T>
class ThreadSafeStack {
public:
void push(const T& value) {
lock_guard<mutex> lock(_mutex);
_stack.push(value);
}
bool try_pop(T& value) {
lock_guard<mutex> lock(_mutex);
if(_stack.empty()) return false;
value = _stack.top();
_stack.pop();
return true;
}
private:
stack<T> _stack;
mutex _mutex;
};
如果需要使用自定义容器作为底层实现,必须确保满足接口要求:
cpp复制template<typename T>
class CustomContainer {
public:
void push_back(const T&);
void pop_back();
T& back();
// ...其他必要接口
};
// 使用自定义容器
stack<int, CustomContainer<int>> custom_stack;
因为vector不支持pop_front()操作。queue必须使用支持前端删除的容器,如deque或list。
解决方案:
cpp复制queue<int> q; // 默认使用deque
cpp复制queue<int, list<int>> q;
这种设计是出于异常安全的考虑。如果有一个函数既返回顶部元素又删除它,那么在拷贝返回值时如果发生异常,元素就已经被删除了,导致数据丢失。
正确用法:
cpp复制while(!s.empty()) {
auto val = s.top(); // 先获取
s.pop(); // 再删除
// 处理val
}
| 选择依据 | 推荐容器 |
|---|---|
| 需要最高性能 | deque(默认) |
| 需要连续内存 | vector(仅stack) |
| 需要稳定迭代器 | list |
| 内存受限环境 | deque |
| 频繁中间访问 | deque |
stack和queue本身不提供迭代器,但如果通过底层容器直接访问,需要注意:
cpp复制stack<int> s;
s.push(1);
s.push(2);
// 危险:直接访问底层容器
auto& con = s._con; // 假设_con是public
for(auto it = con.begin(); it != con.end(); ++it) {
s.push(3); // 可能导致迭代器失效
cout << *it;
}
最佳实践:不要绕过stack/queue的接口直接操作底层容器。
设计一个能在O(1)时间内获取最小元素的栈:
cpp复制template<typename T>
class MinStack {
public:
void push(const T& x) {
_data.push(x);
if(_min.empty() || x <= _min.top()) {
_min.push(x);
}
}
void pop() {
if(_data.top() == _min.top()) {
_min.pop();
}
_data.pop();
}
const T& top() const { return _data.top(); }
const T& min() const { return _min.top(); }
private:
stack<T> _data;
stack<T> _min;
};
使用两个栈模拟队列行为:
cpp复制template<typename T>
class QueueWithStacks {
public:
void push(T x) {
_in.push(x);
}
void pop() {
if(_out.empty()) {
while(!_in.empty()) {
_out.push(_in.top());
_in.pop();
}
}
_out.pop();
}
T front() {
if(_out.empty()) {
while(!_in.empty()) {
_out.push(_in.top());
_in.pop();
}
}
return _out.top();
}
private:
stack<T> _in;
stack<T> _out;
};
在实际开发中,queue常用于线程池的任务队列:
cpp复制class ThreadPool {
public:
ThreadPool(size_t threads) : _stop(false) {
for(size_t i=0; i<threads; ++i) {
_workers.emplace_back([this] {
while(true) {
function<void()> task;
{
unique_lock<mutex> lock(_queue_mutex);
_condition.wait(lock, [this] {
return _stop || !_tasks.empty();
});
if(_stop && _tasks.empty()) return;
task = move(_tasks.front());
_tasks.pop();
}
task();
}
});
}
}
template<class F>
void enqueue(F&& f) {
{
unique_lock<mutex> lock(_queue_mutex);
_tasks.emplace(forward<F>(f));
}
_condition.notify_one();
}
~ThreadPool() {
{
unique_lock<mutex> lock(_queue_mutex);
_stop = true;
}
_condition.notify_all();
for(auto& worker : _workers) {
worker.join();
}
}
private:
vector<thread> _workers;
queue<function<void()>> _tasks;
mutex _queue_mutex;
condition_variable _condition;
bool _stop;
};
通过自己实现stack和queue,不仅能深入理解STL的设计思想,还能根据具体需求进行定制扩展。这种底层实现的理解对于编写高效、稳定的C++代码至关重要。