【C++】 深入容器适配器:从零实现stack与queue

📌 相关专栏

很高兴你点开这篇文章✨

这里会持续更新更多有用的内容,关注我,一起慢慢变好呀

👍 点赞 ⭐ 收藏 💬 评论


文章目录

  • 前言
  • [1. stack:栈容器适配器](#1. stack:栈容器适配器)
    • [1.1 核心特性](#1.1 核心特性)
    • [1.2 常用接口](#1.2 常用接口)
    • [1.3 stack 接口总结](#1.3 stack 接口总结)
  • [2. queue:队列容器适配器](#2. queue:队列容器适配器)
    • [2.1 核心特性](#2.1 核心特性)
    • [2.2 常用接口](#2.2 常用接口)
    • [2.3 queue 接口总结](#2.3 queue 接口总结)
  • [3. stack 与 queue 的模拟实现](#3. stack 与 queue 的模拟实现)
    • [3.1 stack 模拟实现](#3.1 stack 模拟实现)
    • [3.2 queue 模拟实现](#3.2 queue 模拟实现)
    • [3.3 为什么 stack 和 queue 的底层容器默认是 deque?](#3.3 为什么 stack 和 queue 的底层容器默认是 deque?)
  • [4. 按需实例化:模板的惰性编译](#4. 按需实例化:模板的惰性编译)
    • [4.1 概念](#4.1 概念)
    • [4.2 代码示例](#4.2 代码示例)
  • [5. deque:双端队列详解](#5. deque:双端队列详解)
    • [5.1 什么是 deque?](#5.1 什么是 deque?)
    • [5.2 deque 常用接口](#5.2 deque 常用接口)
    • [5.3 deque 接口总结](#5.3 deque 接口总结)
    • [5.4 deque 的适用场景](#5.4 deque 的适用场景)
  • [6. deque 的底层结构:中控器 + 缓冲区](#6. deque 的底层结构:中控器 + 缓冲区)
    • [6.1 整体架构](#6.1 整体架构)
    • [6.2 两个核心组件](#6.2 两个核心组件)
      • [1. 缓冲区(buffer)](#1. 缓冲区(buffer))
      • [2. 中控器(map)](#2. 中控器(map))
    • [6.3 扩容效率对比](#6.3 扩容效率对比)
  • [7. 完整测试用例](#7. 完整测试用例)
    • [Test 1:stack 使用 vector 作为底层容器](#Test 1:stack 使用 vector 作为底层容器)
    • [Test 2:queue 使用 list 作为底层容器](#Test 2:queue 使用 list 作为底层容器)
  • 8.总结
  • 本文全部代码
    • [🐾 Queue.h](#🐾 Queue.h)
    • [🐾 Stack.h](#🐾 Stack.h)
    • [🐾 Test.cpp](#🐾 Test.cpp)

前言

stack(栈)和 queue(队列)是 C++ 标准库中的容器适配器------它们不直接管理内存,而是通过封装底层容器(如 deque、vector、list)来实现特定的访问规则:

容器 访问规则 比喻
stack(栈) 后进先出(LIFO) 叠盘子:后放的先取
queue(队列) 先进先出(FIFO) 排队买票:先来的先服务
  • 而 deque(双端队列)是它们的默认底层容器,兼具数组的随机访问和链表的头尾高效操作。

🐶 🐾 ✨ 🐾 🐶


1. stack:栈容器适配器

1.1 核心特性

栈是一种后进先出(LIFO) 的数据结构,仅能访问栈顶元素,不支持随机访问

  • 访问规则:只能从栈顶添加或删除元素,就像叠盘子------后放的盘子先被取走。
  • 头文件:#include<stack>

1.2 常用接口

cpp 复制代码
#include<stack>
using namespace std;

void test_stack()
{
    stack<int> st;
    st.push(1);      // 入栈:向栈顶添加元素
    st.push(2);
    st.push(3);
    st.push(4);

    while (!st.empty())
    {
        cout << st.top() << " ";   // 获取栈顶元素
        st.pop();                  // 删除栈顶元素
    }
    cout << endl;   // 输出:4 3 2 1
}

1.3 stack 接口总结

接口 作用 注意事项
push(val) 向栈顶添加元素 新元素成为新的栈顶
pop() 删除当前栈顶元素 无返回值,需先确保栈非空
top() 返回栈顶元素的引用 可读可改,需先确保栈非空
size() 返回栈中元素个数 返回 size_t 类型
empty() 判断栈是否为空 常用于遍历或删除前判断

🐶 🐾 ✨ 🐾 🐶


2. queue:队列容器适配器

2.1 核心特性

队列是一种先进先出(FIFO) 的数据结构

  • 访问规则:`从队尾添加元素,从队头删除元素,就像排队买票------先来的先服务。
  • 头文件:#include<queue>

2.2 常用接口

cpp 复制代码
#include<queue>
using namespace std;

void test_queue()
{
    queue<int> q;
    q.push(1);       // 入队列:向队尾添加元素
    q.push(2);
    q.push(3);
    q.emplace(4);    // C++11:原地构造元素

    while (!q.empty())
    {
        cout << q.front() << " ";   // 获取队头元素
        q.pop();                    // 删除队头元素
    }
    cout << endl;   // 输出:1 2 3 4
}

2.3 queue 接口总结

接口 作用 注意事项
push(val) 向队尾添加元素 新元素成为最后一个元素
pop() 删除队头元素 需先通过 front() 获取元素
front() 返回队头元素的引用 可读可改,需确保队列非空
back() 返回队尾元素的引用 可读可改,需确保队列非空
size() 返回队列中元素个数 返回 size_t 类型
empty() 判断队列是否为空 常用于遍历前判断

🐶 🐾 ✨ 🐾 🐶


3. stack 与 queue 的模拟实现

3.1 stack 模拟实现

cpp 复制代码
namespace MyStack
{
    // 模板参数:T 为元素类型,Container 为底层容器类型
    template<class T, class Container = deque<T>>
    class stack
    {
    public:
        void push(const T& x) { _con.push_back(x); }   // 入栈:尾插
        
        void pop() { _con.pop_back(); }                 // 出栈:尾删
        
        const T& top() const { return _con.back(); }   // 栈顶:取尾部元素
        
        bool empty() { return _con.size() == 0; }
        
        size_t size() { return _con.size(); }
        
    private:
        Container _con;   // 底层容器
        				 //_con是Container的缩写
    };
}

🐾 stack 的所有操作都转换为对底层容器的操作:

  • push -> push_back(尾插)
  • pop -> pop_back(尾删)
  • top -> back(取尾部)

3.2 queue 模拟实现

cpp 复制代码
namespace MyQueue
{
    template<class T, class Container = deque<T>>
    class queue
    {
    public:
        void push(const T& x) { _q.push_back(x); }    // 入队:尾插
        
        void pop() { _q.pop_front(); }                 // 出队:头删
        
        const T& front() { return _q.front(); }       // 队头:取头部元素
        
        const T& back() { return _q.back(); }         // 队尾:取尾部元素
        
        bool empty() { return _q.size() == 0; }
        
        size_t size() { return _q.size(); }
        
    private:
        Container _q;
    };
}

🐾 queue 的操作:

  • push -> push_back(尾插)
  • pop -> pop_front(头删)
  • front -> front(取头部)
  • back -> back(取尾部)

3.3 为什么 stack 和 queue 的底层容器默认是 deque?

容器 push_back pop_back push_front pop_front operator
vector O(1) 均摊 O(1) O(n) O(n) O(1)
list O(1) O(1) O(1) O(1) O(n)
deque O(1) O(1) O(1) O(1) O(1)

结论 :deque 在头尾操作上都是 O(1),同时支持随机访问,是 stack 和 queue 最理想的底层容器。

🐶 🐾 ✨ 🐾 🐶


4. 按需实例化:模板的惰性编译

4.1 概念

C++ 模板的一个特性:

一个类被实例化后,不是所有成员函数都会被实例化,只有被调用到的函数才会被编译。


4.2 代码示例

cpp 复制代码
amespace MyStack
{
	// 模板参数:T为元素类型,Container为底层容器类型
	//Container适配转换出stack
	//_con是Container的缩写
	template<class T, class Container = deque<T>>
	class stack
	{
	public:
		//入栈
		void push(const T& x)
		{
			_con.push_back(x);
		}

		//出栈
		void pop()
		{
			_con.pop_back();
		}

		//按需实例化
		//void pop()
		//{
		//	_con.pop_front();
		//	//首先我们要清楚栈是不能出"栈底"元素的,所以这个在功能上就不对
		//	//其次如果我们Container实例化成的是vector容器,而vector是没有pop_front()这个接口的
		//	//所以正常来说是有问题的,但是当我们没有调用该函数运行程序时是不会报错的
		//	//原因就是按需实例化:当一个类被实例化后不是类中所有的成员函数都会实例化,
		//	//而是根据你是否调用了该函数来进行实例化,而没有被实例化的成员函数其内部编译器是不会仔细查看的
		//	//所以即使Container实例化成的是vector容器,只要我们不调用这个函数也不会报错
		//	//但是只要我们调用了该函数编译器就会查找vector容器是否有该接口,若没有就会报错
		//}

		//获取栈顶元素
		const T& top() const
		{
			return _con.back();
			//不管是vector还是list等容器都有通用的获取尾部数据接口back()
			//所以这里必须写成_con.back();这种写法
		}

		//判空
		bool empty()
		{
			return _con.size() == 0;
		}

		//获取数据个数
		size_t size()
		{
			return _con.size();
		}
	private:
		Container _con;
		//_con的类型为模板参数Container,当Container实例化成具体容器时
		//模拟实现stack,_con调用的接口就会转换成对应容器的接口使用
	};

🐶 🐾 ✨ 🐾 🐶


5. deque:双端队列详解

  • vector(是数组) :物理空间连续,可支持快速下标随机访问,尾插、尾删效率极高(但是头删和中间位置随机删除/插入效率低下(时间复杂度为O(N)),且插入可能触发扩容)

    • 适用场景:适合基于随机访问的排序算法
  • list(链表) :物理空间不连续,不支持快速下标随机访问,数据访问效率差(任意位置插入/删除效率极高(时间复杂度为O(1);插入无需扩容,可按需申请空间与释放内存)

  • deque(顺序容器) :双端队列,是STL中一种双开口的"连续"空间数据结构,将stack和list的功能进行取长补短(即用一个数组来存放指针,而指针的内容是一段连续的空间)

5.1 什么是 deque?

deque(double-ended queue,双端队列)是 STL 中的一种双开口的连续空间数据结构,融合了 vector 和 list 的优点:

  • 两端高效操作 :头尾插入/删除时间复杂度 O(1)
  • 随机访问 :支持 运算符,时间复杂度 O(1)
  • 动态扩容:不需移动全部元素,效率高于 vector
  • 迭代器稳定性:两端插入/删除不使迭代器失效

5.2 deque 常用接口

cpp 复制代码
void test_deque1()
{
    deque<int> dq;

    // 尾插
    dq.push_back(3);
    dq.push_back(4);
    dq.push_back(5);      // 3 4 5

    // 头插
    dq.push_front(2);
    dq.push_front(1);     // 1 2 3 4 5

    // 随机访问
    dq[4] *= 10;          // 1 2 3 4 50

    for (auto e : dq)
    {
        cout << e << " ";
    }
    cout << endl;         // 1 2 3 4 50
}

5.3 deque 接口总结

接口 作用
push_front(val) 在队头插入元素
push_back(val) 在队尾插入元素
pop_front() 删除队头元素
pop_back() 删除队尾元素
front() 获取队头元素的引用
back() 获取队尾元素的引用
operator[](i) 随机访问第 i 个元素
size() 返回元素个数
empty() 判断是否为空
clear() 清空所有元素

5.4 deque 的适用场景

适合只需头尾操作、无需频繁遍历的场景

🐶 🐾 ✨ 🐾 🐶


6. deque 的底层结构:中控器 + 缓冲区

6.1 整体架构

deque 不是真正连续的内存,而是通过中控器管理多个缓冲区来实现逻辑上的连续:


6.2 两个核心组件

1. 缓冲区(buffer)

  • 存储实际数据的最小单元
  • 固定大小的内存块(如默认 512 字节)
  • 每个缓冲区独立存在
  • 装满后申请新缓冲区,不迁移旧数据

2. 中控器(map)

  • 本质是一个动态数组(指针数组)
  • 存储各个缓冲区的首地址
  • 当中控器装满时,申请更大的中控器
  • 只需拷贝指针,无需迁移缓冲区中的实际数据

6.3 扩容效率对比

容器 扩容方式 效率
vector 申请新内存 -> 移动所有元素 -> 释放旧内存 O(n)
deque 申请新缓冲区 -> 中控器新增指针 O(1) 均摊

🐶 🐾 ✨ 🐾 🐶


7. 完整测试用例

Test 1:stack 使用 vector 作为底层容器

cpp 复制代码
void test_stack1()
{
    // 基于 vector 的栈(也可以使用 list、deque)
    MyStack::stack<int, vector<int>> st;
    st.push(1);
    st.push(2);
    st.push(3);
    st.push(4);

    while (!st.empty())
    {
        cout << st.top() << " ";   // 4 3 2 1
        st.pop();
    }
}

Test 2:queue 使用 list 作为底层容器

cpp 复制代码
void test_queue1()
{
    // 基于 list 的队列(vector 不适合,因为没有 pop_front)
    MyQueue::queue<int, list<int>> q1;
    q1.push(1);
    q1.push(2);
    q1.push(3);
    q1.push(4);

    while (!q1.empty())
    {
        cout << q1.front() << " ";   // 1 2 3 4
        q1.pop();
    }
}

🐶 🐾 ✨ 🐾 🐶


8.总结

🐾 stack、queue 和 deque 的核心知识:

容器 核心特性 适用场景
stack LIFO,只能栈顶操作 函数调用栈、括号匹配、逆序输出
queue FIFO,队尾入队头出 任务队列、BFS 算法、消息队列
deque 双端高效操作 + 随机访问 作为 stack/queue 的底层容器

🐾 三种容器该如何选择?

需求 适配容器
需要后进先出 使用 stack
需要先进先出 使用 queue
需要头尾操作 + 随机访问 使用 deque
只需要头尾操作 使用 deque 或 list

🐾 容器适配器的设计思想:

  • stack 和 queue 不直接管理内存
  • 通过封装底层容器实现特定访问规则
  • 用户可以自定义底层容器(只要支持所需接口)

🐶 🐾 ✨ 🐾 🐶


本文全部代码

🐾 Queue.h

cpp 复制代码
#pragma once
//Queue(队列):先进先出
// 
//访问规则:从"队尾"添加元素,从"队头"删除元素(最先入队的元素最先入队)
// 
//头文件:#include<queue>

//----------------------常用接口--------------------------------------------------------
//push(val):向队列的"队尾"添加一个元素,新元素成为队列的最后一个元素,操作后队列长度+1
// 
//pop():删除队列的"队头"元素(即最早入队的元素),操作后队列长度-1,无返回值(需先通过front()获取队头元素再删除)
// 
//front():返回队列"队头"元素的引用(可读可改),仅访问不删除,需确保队列非空
// 
//back():返回队列"队尾"元素的引用(可读可改),仅访问不删除,需确保队列非空
// 
//size():返回队列中当前元素总个数,返回值类型为size_t(无符号整数)
// 
//empty():判断队列是否为空:若队列中无元素则返回true,有元素则返回false,常用于遍历或删除前判断队列状态

//基本用法代码演示
				//#include<queue>
				//using namespace std;

				//void test_queue()
				//{
				//	queue<int> a;
				//	a.push(1);
				//	a.push(2);
				//	a.push(3);
				//	a.emplace(4);

				//	while (!a.empty())
				//	{
				//		cout << a.front() << " ";
				//		a.pop();

				//	}
				//	cout << endl;
				//}

				//int mian()
				//{
				//	test_queue();
				//	return 0;
				//}
/////////////////////////////////////////////////////////////////////////////////////////////////////
namespace MyQueue
{
	template<class T, class Container = deque<T>>
	class queue
	{
	public:
		//入队列
		void push(const T& x)
		{
			_q.push_back(x);
		}

		//出队列
		void pop()
		{
			_q.pop_front();
			//通过这个接口我们就能知道vector是不适合作为队列的底层容器的
			//首先在实现上vector本身就没有pop_front()这个接口,无法调用
			//其次队列出数据是头部出,而vector头删数据的效率是非常低的(时间复杂度为O(N))
			//所以队列的实现我们就不会用vector作为底层容器
		}

		//获取队头数据
		const T& front()
		{
			return _q.front();
		}

		//获取队尾数据
		const T& back()
		{
			return _q.back();
		}

		//判空
		bool empty()
		{
			return _q.size() == 0;
		}

		//获取数据个数
		size_t size()
		{
			return _q.size();
		}
	private:
		Container _q;
	};
}

🐾 Stack.h

cpp 复制代码
#pragma once

//stack(栈):后进后出,仅能访问栈顶元素,不支持随机访问
// 
//访问规则:只能从栈顶添加或删除元素(最后入栈的元素最先出栈),可理解成水杯
// 
//头文件:#include<stack>
//-------------------常用接口使用--------------------------------------------
//push(val):向栈顶添加元素,新元素成为新的栈顶
// 
//pop():删除当前栈顶元素(操作后原栈顶元素的下一个元素成为新的元素栈顶,无返回值,需先确保栈是非空的)
// 
//top():返回栈顶元素的引用(可直接读取或修改栈顶值),也需先确保栈是非空的
// 
//size();返回栈中当前储存的元素总个数,返回值为无符号整数(size_t)
// 
//empty();判断是否为空,若栈中无元素则返回true,fouze返回false

		////基本用法代码演示:
				//#include<stack>
				//using namespace std;
				//void test_stack()
				//{
				//	stack<int> st;
				//	st.push(1);
				//	st.push(2);
				//	st.push(3);
				//	st.push(4);
				//	
				//	while (!st.empty())
				//	{
				//		cout << st.top() << " ";
				//		st.pop();
				//	}
				//	cout << endl;
				//}
				//
				//int main()
				//{
				//	test_stack();
				//}
				// 
				// 
				// 
//实例化:指的是C++模板(包括函数模板和类模板)的代码,只有真正被使用的时候,编译器才会为其生成具体的类型版本
// 
////////////////////////////////////////////////////////////////////////////////////////////////////////
namespace MyStack
{
	// 模板参数:T为元素类型,Container为底层容器类型
	//Container适配转换出stack
	//_con是Container的缩写
	template<class T, class Container = deque<T>>
	class stack
	{
	public:
		//入栈
		void push(const T& x)
		{
			_con.push_back(x);
		}

		//出栈
		void pop()
		{
			_con.pop_back();
		}

		//按需实例化
		//void pop()
		//{
		//	_con.pop_front();
		//	//首先我们要清楚栈是不能出"栈底"元素的,所以这个在功能上就不对
		//	//其次如果我们Container实例化成的是vector容器,而vector是没有pop_front()这个接口的
		//	//所以正常来说是有问题的,但是当我们没有调用该函数运行程序时是不会报错的
		//	//原因就是按需实例化:当一个类被实例化后不是类中所有的成员函数都会实例化,
		//	//而是根据你是否调用了该函数来进行实例化,而没有被实例化的成员函数其内部编译器是不会仔细查看的
		//	//所以即使Container实例化成的是vector容器,只要我们不调用这个函数也不会报错
		//	//但是只要我们调用了该函数编译器就会查找vector容器是否有该接口,若没有就会报错
		//}

//-------------------------"_con是Container的缩写"----------------------------------


		//获取栈顶元素
		const T& top() const
		{
			return _con.back();
			//不管是vector还是list等容器都有通用的获取尾部数据接口back()
			//所以这里必须写成_con.back();这种写法
		}

		//判空
		bool empty()
		{
			return _con.size() == 0;
		}

		//获取数据个数
		size_t size()
		{
			return _con.size();
		}
	private:
		Container _con;
		//_con的类型为模板参数Container,当Container实例化成具体容器时
		//模拟实现stack,_con调用的接口就会转换成对应容器的接口使用
	};
}

🐾 Test.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<list>
#include<deque>
using namespace std;

#include"Stack.h"
#include"Queue.h"

void test_stack1()
{
	//基于底层容器为数组的栈
	//此时模板参数T实例化成int,Container实例化成vector<int>
	MyStack::stack<int, vector<int>> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	cout << endl;
}
//int main()
//{
//	test_stack1();
//	return 0;
//}

//////////////////////////////////////////////////////////////////////////////////////////////////////

void test_queue1()
{
	//MyQueue::queue<int, vector<int>> q1;
	MyQueue::queue<int, list<int>> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	while (!q1.empty())
	{
		cout << q1.front() << " ";
		q1.pop();
	}
	cout << endl;
}

//int main()
//{
//	test_queue1();
//	return 0;
//}

//////////////////////////////////////////////////////////////////////////////////////////////////////

//vector(是数组):物理空间连续,可支持快速下标随机访问,尾插、尾删效率极高
//					但是头删和中间位置随机删除/插入效率低下(时间复杂度为O(N)),且插入可能触发扩容
//		适用场景:适合基于随机访问的排序算法
// 
//list(链表):物理空间不连续,不支持快速下标随机访问,数据访问效率差
//				任意位置插入/删除效率极高(时间复杂度为O(1);插入无需扩容,可按需申请空间与释放内存
//		
//--deque(顺序容器):双端队列,是STL中一种双开口的"连续"空间数据结构,将stack和list的功能进行取长补短
//					即用一个数组来存放指针,而指针的内容是一段连续的空间
//1.两端高效操作:在队头(push_front/pop_front)和队尾(push_front/pop_back)插入/删除元素的时间复杂度都是O(1)
// 
//2.随机访问:支持[] 运算符和 at() 方法,像数组一样直接访问任意位置的元素,时间复杂度为O(1)
// 
//3.动态扩容:deque的内存结构是分段的(不像vector是连续的一块内存),扩容时不需要移动全部元素,效率更高
// 
//4.迭代器稳定性:在两端插入/删除元素时不会使原有迭代器失效(中间插入/删除会失效)

//--deque的底层结构
//1.缓冲区(buffer):存储实际数据的最小单元,是固定大小的内存块(比如默认大小为512字节),每个缓冲区独立存在
//				当数据装满一个缓冲区后,会申请一个新的缓冲区,不会像vector一样扩容式要整体迁移旧数据

//2.中控器(map):本质是一个动态数组,存储的是各个缓冲区分首地址(指针数组)当缓冲区数量增加导致中控器装满时,
//				会申请一个更大的中控器(类似 vector 扩容),但只需拷贝旧中控器中存储的 "缓冲区地址",
//				无需迁移缓冲区中的实际数据,效率远高于 vector 扩容。
// 
// --deque的适用场景:适合只需头尾操作、无需频繁遍历的场景
// 
//-----------------------------"deque常用的接口"---------------------------------------------
//push_fornt()	;在队头插入元素
//push_back()	:在队尾插入元素
//pop_front()	:删除队头元素
//pop_back()	:删除队尾元素
//front()		:获取队头元素的引用
//back()		:获取队尾元素的引用
//size()		:返回元素个数
//empty()		:判断是否为空
//clear()		:清空所有元素
//operator()	:随机访问元素

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void test_deque1()
{
	deque<int> dq;
	//尾插
	dq.push_back(3);
	dq.push_back(4);
	dq.push_back(5);		//3 4 5

	//头插
	dq.push_front(2);
	dq.push_front(1);		//1 2 3 4 5

	//下标为4(即第五个元素)的元素*5
	dq[4] *= 10;			//1 2 3 4 50

	for (auto e : dq)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
	test_deque1();
	return 0;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////


//int main()
//{ 
//	//test_stack1();
//	//test_queue1();
//	//test_deque1();
//	return 0;
//}

🐶 🐾 ✨ 🐾 🐶


  1. 欢迎留言交流
  2. 期待你的评论与建议
  3. 留下你的想法吧

谢谢你看到这里呀

如果喜欢这篇内容,点个关注,下次更新不迷路✨

👍 点赞 ⭐ 收藏 💬 评论