C++ :容器适配器stack源码级拆解

std::stack 是 C++ STL 中典型的容器适配器(Container Adapter),它本身不实现底层存储逻辑,而是对已有顺序容器进行封装,对外提供严格的「后进先出(LIFO,Last In First Out)」语义。它是 STL 中最简单的适配器之一,也是面试中「容器适配器」考点的核心载体。


一、标准定义与基本结构

1. 头文件与模板声明

头文件:<stack>,位于 std 命名空间。

标准模板签名:

cpp 复制代码
template <class T, class Container = std::deque<T>>
class stack;
  • T:存储元素的类型
  • Container:底层存储容器,默认使用 std::deque<T>,可手动指定为满足要求的顺序容器

2. 底层容器的准入条件

能作为 stack 底层的容器,必须支持以下尾部操作接口:

  • back():访问尾部元素
  • push_back():尾部插入元素
  • pop_back():尾部删除元素
  • empty():判空
  • size():获取元素个数

STL 中天然满足要求的顺序容器有 3 个:std::deque(默认)、std::vectorstd::list

3. 核心成员变量

标准库的绝大多数实现(如 libstdc++、MSVC STL)中,stack 只有一个受保护成员:底层容器对象,命名通常为 c

cpp 复制代码
template <class T, class Container>
class stack {
protected:
    Container c; // 底层容器对象,所有栈操作都委托给它
public:
    // 所有对外接口都通过调用 c 的对应方法实现
};

这就是「容器适配器」的本质:完全委托底层容器实现功能,自身只做接口裁剪与语义约束,是典型的适配器设计模式。


二、为什么默认底层容器是 std::deque?

这是 STL 设计的经典面试题。stack 只在尾部操作,理论上 vector、list 都能用,最终选择 deque 是综合性能、稳定性、内存特性的最优解。

1. 对比 vector:避免扩容性能抖动

  • vector 尾插是「均摊 O(1)」,但容量不足时需要重新分配整块连续内存 + 拷贝所有元素,扩容过程有明显性能开销,且会造成内存峰值。
  • deque 采用分段连续存储,扩容时只需要新增一个数据块,不需要移动已有元素,push_back稳定 O(1),无性能抖动,更符合栈的稳定操作预期。
  • vector 需要大块连续内存,大栈场景下容易出现内存分配失败;deque 分段存储,内存利用率更高。

2. 对比 list:更高的缓存与空间效率

  • list 每个节点都要额外存储前后指针,内存开销大;且节点地址不连续,缓存命中率极低。
  • deque 的数据块是连续的,缓存局部性远优于 list;额外指针开销远小于 list。
  • stack 只操作尾部,不需要 list 的中间插入能力,用 list 属于无意义的性能浪费。

3. 综合结论

deque 完美匹配栈的「仅尾部操作」特性:稳定 O(1) 的双端操作、无扩容抖动、内存开销低、缓存表现优秀,因此成为默认底层容器。


三、核心接口的源码级实现

stack 的所有接口都是对底层容器的直接调用,没有额外复杂逻辑,下面是核心函数的等价标准实现。

1. 构造与析构

cpp 复制代码
// 默认构造:调用底层容器默认构造
stack() : c() {}

// 拷贝构造:拷贝整个底层容器
stack(const stack& other) : c(other.c) {}

// 从已有容器构造:直接拷贝容器内容
explicit stack(const Container& cont) : c(cont) {}

// 析构:自动调用底层容器析构,无需手动处理内存
~stack() = default;

注意:单参数的容器构造函数加了 explicit,禁止隐式类型转换,避免语义歧义。

2. 栈顶元素操作

cpp 复制代码
// 压栈:尾部插入元素
void push(const T& value) {
    c.push_back(value);
}

// 弹栈:尾部删除元素,无返回值
void pop() {
    c.pop_back();
}

// 访问栈顶元素:返回尾部元素的引用
T& top() {
    return c.back();
}
const T& top() const {
    return c.back();
}
经典面试点:pop() 为什么不返回元素?

这是典型的异常安全设计 : 如果 pop()返回元素,需要在弹出后将元素拷贝给调用方;如果拷贝构造过程中抛出异常,元素已经从栈中移除,就会永久丢失。 拆分为 top()(拿引用,无拷贝风险)+ pop()(只删除,不拷贝),即使拷贝失败,元素仍然保留在栈中,不会丢失。

3. 容量与判空

cpp 复制代码
bool empty() const {
    return c.empty();
}

size_t size() const {
    return c.size();
}

4. 比较运算符

stack 的比较直接委托给底层容器的比较,按字典序比较全部元素:

cpp 复制代码
template <class T, class Container>
bool operator==(const stack<T, Container>& lhs, const stack<T, Container>& rhs) {
    return lhs.c == rhs.c;
}

template <class T, class Container>
bool operator<(const stack<T, Container>& lhs, const stack<T, Container>& rhs) {
    return lhs.c < rhs.c;
}

四、stack 的核心特性与限制

1. 严格的 LIFO 语义,接口极度精简

stack 刻意裁剪了底层容器的绝大多数接口:

  • 不支持随机访问(无 operator[]at()
  • 不支持中间插入、删除
  • 不支持遍历:没有迭代器(iterator) ,也没有 begin()/end() 接口

如果一定要遍历,也不是没有办法。可以通过获取到底层container的引用对象,然后操作container的方式来实现遍历。例如:

cpp 复制代码
auto tranverse = [](const std::stack<int, std::deque<int> >& st) {
		const std::deque<int>& ref_st = st._Get_container(); / / 获取容器的常引用对象
		std::deque<int>::size_type  size = ref_st.size(); 		
     	std::cout << "container size =" << st.size() << std::endl;
     	for (std::deque<int>::size_type idx = 0; idx < size; ++idx) { 			
     		std::cout << ref_st[idx] << " ,"; 
     	} 		
     	std::cout << std::endl; 
};

std::stack<int, std::deque<int> > st; 	
for (int i = 0; i < 5; ++i) st.push(i); 	
tranverse(st); 	
 	
st.pop(); 
tranverse(st);
  • 只能访问栈顶元素,只能在栈顶插入/删除

这种设计的目的是强制使用者遵守栈的语义,避免误用。

2. 时间复杂度

所有操作的时间复杂度与底层容器对应操作一致:

  • push() / pop() / top() / empty() / size():默认 deque 下均为 O(1)
  • 析构、拷贝赋值为 O(n),属于容器生命周期操作

3. 迭代器失效

stack 不对外暴露迭代器,因此用户层面不存在迭代器失效问题;底层容器自身的迭代器失效规则仍然存在,但被适配器完全屏蔽。


五、自定义底层容器的用法

如果有特殊需求,可以手动指定底层容器,语法如下:

cpp 复制代码
// 用 vector 做底层的栈
std::stack<int, std::vector<int>> vec_stack;

// 用 list 做底层的栈
std::stack<int, std::list<int>> list_stack;

适用场景

  • 元素体积小、栈深度可控,追求极致缓存性能:可选用 vector
  • 需要频繁插入删除大对象,不希望移动元素:可选用 list
  • 绝大多数通用场景:默认 deque 是最优解,无需修改

六、高频面试题总结

  1. Q:std::stack 是容器吗?

    A:不是,它是容器适配器,本身不实现存储,只封装底层容器、提供栈语义。

  2. Q:stack 默认底层容器是什么?为什么选它?

    A:默认是 std::deque。原因:尾部操作稳定 O(1)、无扩容抖动、内存开销低、缓存表现优于 list。

  3. Q:pop() 为什么设计成 void 不返回值?

    A:出于异常安全考虑。如果返回值时拷贝构造抛出异常,元素已弹出会永久丢失;拆分为 top() 读引用 + pop() 删除,保证异常安全。

  4. Q:stack 支持迭代器吗?为什么?

    A:不支持。栈的语义是「只能访问栈顶」,遍历不符合 LIFO 语义,因此刻意不提供迭代器接口。

  5. Q:stack 的底层可以用哪些容器?需要满足什么条件?

    A:支持 back()、push_back()、pop_back()、empty()、size() 的顺序容器都可以,STL 原生支持 deque、vector、list。

相关推荐
美味蛋炒饭.1 小时前
Git 版本控制(下)
开发语言·git·学习·总结·后端开发
我星期八休息2 小时前
网络编程—网络层
开发语言·前端·网络·人工智能·智能路由器
不负岁月无痕2 小时前
简单理解操作系统结构
java·linux·c语言·开发语言·c++·面试
余额瞒着我当琳2 小时前
C++模板初阶与STL初探
android·java·c++
SomeB1oody2 小时前
【RustyML入门】2.9. MeanShift
开发语言·后端·机器学习·rust·教程
萧瑟其中~2 小时前
多线程锁详解:互斥锁·自旋锁·读写锁(CAS + futex 原理)
开发语言·c++
初级代码游戏3 小时前
iOS开发 Swift 速记7:结构体和类
开发语言·ios·swift
golang学习记3 小时前
Go 项目使用docker compose的正确方式
开发语言·docker·golang