【C++修炼之路】C++容器:stack、queue、deque 基本用法详解

🏝️专栏: 【C++修炼之路】

🌅主页: f狐o狸x

"星星永远不会惧怕黑暗,因为越黑暗星星越闪耀"


目录

一、先理清三者的核心关系与基础认知

二、基础使用前提:头文件与命名空间

三、deque(双端队列):双端高效操作的序列容器

[3.1. deque的核心特性](#3.1. deque的核心特性)

[3.2. deque的常用构造函数(4种高频方式)](#3.2. deque的常用构造函数(4种高频方式))

[3.3. deque的常用成员函数(核心操作)](#3.3. deque的常用成员函数(核心操作))

(1)元素插入(双端+中间)

(2)元素删除(双端+中间)

(3)元素访问与修改(随机访问+双端访问)

(4)容器大小与状态管理

(5)迭代器遍历示例

四、stack(栈):后进先出的容器适配器

[4.1. stack的核心特性](#4.1. stack的核心特性)

[4.2. stack的常用构造函数](#4.2. stack的常用构造函数)

[3. stack的常用成员函数(核心操作)](#3. stack的常用成员函数(核心操作))

五、queue(队列):先进先出的容器适配器

[5.1. queue的核心特性](#5.1. queue的核心特性)

[5.2. queue的常用构造函数](#5.2. queue的常用构造函数)

[5.3. queue的常用成员函数(核心操作)](#5.3. queue的常用成员函数(核心操作))

六、常见误区与注意事项

七、实战案例:三者的典型应用

案例1:stack实现括号匹配(判断字符串中的括号是否成对有效)

案例2:queue实现简单消息队列(先进先出处理消息)

案例3:deque实现滑动窗口(输出数组中每个窗口的最大值,简化版)

八、三者核心特性对比表格

总结


在C++ STL标准模板库中,stack(栈)、queue(队列)、deque(双端队列)是三组高频使用的线性容器,其中stack和queue并非「原生序列容器」,而是基于容器适配器实现的特殊数据结构,且它们的默认底层容器正是deque。这三者各有独特的访问规则和适用场景,本文将从核心特性出发,逐步讲解它们的构造、常用操作及实战用法,适合C++初学者快速上手,同时兼顾知识的连贯性和实用性。

一、先理清三者的核心关系与基础认知

在正式学习用法前,先建立对这三个容器的整体认知,避免混淆核心概念:

  1. deque(双端队列):原生序列容器,底层是「分段连续内存」(并非完全连续,却能模拟连续内存的效果),支持双端高效插入/删除,也支持随机访问,是stack和queue的默认底层支撑容器。

  2. stack(栈):容器适配器,基于「后进先出(LIFO, Last In First Out)」规则工作,仅允许在容器的一端进行插入和删除操作,默认以deque为底层容器。

  3. queue(队列):容器适配器,基于「先进先出(FIFO, First In First Out)」规则工作,仅允许在容器的一端插入、另一端删除,默认同样以deque为底层容器。

简单比喻:

  • deque像一个「两端都能进出的管道」;

  • stack像一个「只有顶端能进出的瓶子」(后放进去的东西先拿出来);

  • queue像一个「排队买票的队伍」(先排队的人先买票,后排队的人后买票)。

二、基础使用前提:头文件与命名空间

使用这三个容器前,必须包含对应的头文件,若想简化代码,可引入std命名空间(否则需显式使用std::前缀):

cpp 复制代码
// 分别包含对应头文件,按需引入
 #include <deque> // deque 头文件 
#include <stack> // stack 头文件 
#include <queue> // queue 头文件 
using namespace std; 

三、deque(双端队列):双端高效操作的序列容器

deque是「double-ended queue」的缩写,作为原生序列容器,它弥补了vector(仅尾部高效)和list(不支持随机访问)的部分短板,具备独特的优势。

3.1. deque的核心特性

  • 底层:分段连续内存,通过中控器维护各段内存的指针,兼顾连续内存的访问效率和链表的内存灵活性。

  • 插入/删除:两端(头部、尾部)插入/删除效率极高,时间复杂度O(1);中间插入/删除效率较低,时间复杂度O(n)(需移动元素)。

  • 访问:支持随机访问 (通过下标[]at()),时间复杂度O(1),区别于list的双向迭代器。

  • 迭代器:支持随机访问迭代器(可进行++--+-操作,如it+3),可正向、反向遍历。

  • 适用场景:需要频繁在两端操作元素,且偶尔需要随机访问的场景(如滑动窗口、缓存队列)。

3.2. deque的常用构造函数(4种高频方式)

和list、vector的构造方式高度相似,初学者可快速迁移知识:

cpp 复制代码
// 1. 空构造(默认构造):创建一个空的deque,存储int类型元素 
deque<int> dq;
// 2. 填充构造:创建包含n个相同值的deque 
deque<int> dq1(5, 10); 
// 5个元素,每个值为10 
deque<int> dq2(3); 
// 3个元素,每个值为int默认值0 
// 3. 迭代器范围构造:通过其他容器的迭代器范围复制构造 
vector<int> vec = {1,2,3,4,5}; 
deque<int> dq3(vec.begin(), vec.end()); 
// 复制vec所有元素 
deque<int> dq4(vec.begin(), vec.begin()+3); 
// 复制前3个元素 
// 4. 拷贝构造:通过另一个deque复制构造 
deque<int> dq5(5, 10); 
deque<int> dq6(dq5); 
// dq6与dq5完全一致

3.3. deque的常用成员函数(核心操作)

deque的成员函数兼具vector和list的部分特性,核心操作可分为「增、删、查、改、容器管理」五类:

(1)元素插入(双端+中间)

cpp 复制代码
deque<int> dq; 
// 1. 尾部插入:push_back(),O(1) 
dq.push_back(10); 
dq.push_back(20); 
// 2. 头部插入:push_front(),O(1)(vector无此函数) 
dq.push_front(5); 
dq.push_front(1); 
// 3. 中间插入:insert(),O(n)(需移动元素) 
// 迭代器指向第2个元素(值为5),插入15 
deque<int>::iterator it = ++dq.begin(); 
dq.insert(it, 15); // 此时dq内容:1 15 5 10 20

(2)元素删除(双端+中间)

cpp 复制代码
deque<int> dq = {1,15,5,10,20}; 
// 1. 尾部删除:pop_back(),O(1),删除20 
dq.pop_back(); 
// 2. 头部删除:pop_front(),O(1),删除1 
dq.pop_front(); 
// 3. 中间删除:erase(),O(n) 
// 删除迭代器指向的元素(15),返回下一个元素的迭代器 
it = dq.begin(); it = dq.erase(it);
// 4. 清空所有元素:clear(),O(n)
dq.clear();

(3)元素访问与修改(随机访问+双端访问)

deque支持两种访问方式,满足不同场景需求:

cpp 复制代码
deque<int> dq = {1,2,3,4,5}; 
// 方式1:随机访问(下标[] 或 at()),O(1) 
cout << "第3个元素(下标2):" << dq[2] << endl; 
// 输出3(无越界检查) 
cout << "第4个元素(下标3):" << dq.at(3) << endl; 
// 输出4(有越界检查,更安全) 
// 方式2:双端访问(front() 头部、back() 尾部),O(1) 
cout << "头部元素:" << dq.front() << endl; 
// 输出1 
cout << "尾部元素:" << dq.back() << endl; 
// 输出5 
// 修改元素(两种方式均可) 
dq[2] = 30; 
// 下标修改,将3改为30 
dq.front() = 10; 
// 头部修改,将1改为10 
dq.back() = 50; 
// 尾部修改,将5改为50 
// 此时dq内容:10 2 30 4 50

(4)容器大小与状态管理

cpp 复制代码
deque<int> dq = {1,2,3}; 
// 1. 获取元素个数:size(),O(1) 
cout << "元素个数:" << dq.size() << endl; 
// 输出3 
// 2. 判断是否为空:empty(),O(1) 
cout << "是否为空:" << dq.empty() << endl; 
// 输出0(false) 
// 3. 调整容器大小:resize() 
dq.resize(5, 100); 
// 调整为5个元素,新增元素为100,结果{1,2,3,100,100} 
dq.resize(2); 
// 调整为2个元素,删除多余元素,结果{1,2}

(5)迭代器遍历示例

cpp 复制代码
#include <deque> 
#include <iostream> 
using namespace std; 
int main() 
{ 
    deque<int> dq = {10,20,30,40,50}; 
    // 1. 正向遍历(随机访问迭代器) 
    cout << "正向遍历:"; 
    for (deque<int>::iterator it = dq.begin(); it != dq.end(); it++) 
    { 
        cout << *it << " ";
    } 
    cout << endl; 
    // 2. 反向遍历 
    cout << "反向遍历:"; 
    for (deque<int>::reverse_iterator it = dq.rbegin(); it != dq.rend(); it++)
    { 
        cout << *it << " "; 
    } 
    cout << endl; 
    // 3. 下标遍历(支持随机访问的优势)     
    cout << "下标遍历:"; 
    for (int i = 0; i < dq.size(); i++) 
    { cout << dq[i] << " "; }     
    cout << endl; return 0; 
}

输出结果

四、stack(栈):后进先出的容器适配器

stack是一种「适配器容器」,它不直接实现数据存储,而是封装了底层容器(默认deque),并提供专属的接口,强制限制只能在「栈顶」进行操作,符合后进先出的规则。

4.1. stack的核心特性

  • 访问规则:仅允许在栈顶(容器的一端)进行插入、删除和访问操作,无法访问栈中间或栈底的元素。

  • 底层容器:默认使用deque,也可指定vector或list作为底层容器(如stack<int, vector<int>> st;)。

  • 无迭代器:不支持迭代器遍历,无法通过循环遍历所有元素(只能通过弹出栈顶元素的方式逐个获取)。

  • 时间复杂度:栈顶的插入(push)、删除(pop)、访问(top)操作均为O(1)。

  • 适用场景:需要后进先出逻辑的场景(如括号匹配、函数调用栈、表达式求值、逆序输出数据)。

4.2. stack的常用构造函数

stack的构造方式较为简单,主要有两种:

cpp 复制代码
// 1. 空构造(默认构造):创建一个空的栈,默认底层容器为deque 
stack<int> st; 
// 2. 拷贝构造:通过另一个stack复制构造 
stack<int> st1; 
st1.push(10); 
st1.push(20);
stack<int> st2(st1); 
// st2与st1完全一致,包含10、20(栈顶为20)

3. stack的常用成员函数(核心操作)

stack的接口非常简洁,仅提供与栈顶相关的操作,无复杂逻辑,初学者容易掌握:

cpp 复制代码
#include <stack> 
#include <iostream> 
using namespace std; 
int main() 
{ 
    stack<int> st; 
    // 1. 栈顶插入元素:push(elem),O(1)(压栈) 
    st.push(10); 
    st.push(20); 
    st.push(30); 
    // 此时栈内元素(从栈底到栈顶):10 → 20 → 30 
    // 2. 访问栈顶元素:top(),O(1)(仅查看,不删除) 
    cout << "当前栈顶元素:" << st.top() << endl; 
    // 输出30 
    // 3. 删除栈顶元素:pop(),O(1)(出栈,无返回值,仅删除) 
    st.pop(); 
    // 删除30,栈顶变为20 
    cout << "出栈后栈顶元素:" << st.top() << endl; 
    // 输出20 
    // 4. 获取元素个数:size(),O(1) 
    cout << "栈内元素个数:" << st.size() << endl; 
    // 输出2 
    // 5. 判断栈是否为空:empty(),O(1) 
    cout << "栈是否为空:" << st.empty() << endl; 
    // 输出0(false) 
    // 6. 清空栈(stack无clear()函数,需通过循环pop()实现) 
    while (!st.empty()) 
    { st.pop(); } 
    cout << "清空后栈是否为空:" << st.empty() << endl; 
    // 输出1(true) 
    return 0; 
}

输出结果

注意:

  1. pop()函数无返回值 ,仅删除栈顶元素,若需要获取栈顶元素,需先调用top()查看,再调用pop()删除。

  2. stack没有clear()成员函数,清空栈需通过循环调用pop(),直到empty()返回true。

  3. 当栈为空时,调用top()pop()会触发未定义行为,建议操作前先用empty()判断。

五、queue(队列):先进先出的容器适配器

queue同样是「适配器容器」,默认底层容器为deque,它强制限制「一端插入、另一端删除」,符合先进先出的规则,类似日常生活中的排队场景。

5.1. queue的核心特性

  • 访问规则:队尾(back)插入元素,队头(front)删除元素,仅能访问队头和队尾元素,无法访问队列中间元素。

  • 底层容器:默认使用deque,也可指定list作为底层容器(如queue<int, list<int>> q;,不推荐vector,因为vector头部删除效率低)。

  • 无迭代器:不支持迭代器遍历,无法通过循环直接遍历所有元素(只能通过弹出队头元素的方式逐个获取)。

  • 时间复杂度:队尾插入(push)、队头删除(pop)、队头/队尾访问均为O(1)。

  • 适用场景:需要先进先出逻辑的场景(如任务队列、消息队列、排队模拟、广度优先搜索(BFS))。

5.2. queue的常用构造函数

和stack类似,queue的构造方式也较为简洁:

cpp 复制代码
// 1. 空构造(默认构造):创建一个空的队列,默认底层容器为deque 
queue<int> q; 
// 2. 拷贝构造:通过另一个queue复制构造 
queue<int> q1; 
q1.push(10); 
q1.push(20); 
queue<int> q2(q1); 
// q2与q1完全一致,包含10、20(队头10,队尾20)

5.3. queue的常用成员函数(核心操作)

queue的接口同样简洁,仅提供与队头、队尾相关的操作,逻辑清晰:

cpp 复制代码
#include <queue> 
#include <iostream> 
using namespace std; 
int main() 
{ 
    queue<int> q; 
    // 1. 队尾插入元素:push(elem),O(1)(入队) 
    q.push(10); 
    q.push(20); 
    q.push(30); 
    // 此时队列元素(从队头到队尾):10 → 20 → 30 
    // 2. 访问队头元素:front(),O(1)(仅查看,不删除) 
    cout << "当前队头元素:" << q.front() << endl; 
    // 输出10 
    // 3. 访问队尾元素:back(),O(1)(仅查看,不删除) 
    cout << "当前队尾元素:" << q.back() << endl; 
    // 输出30
    // 4. 删除队头元素:pop(),O(1)(出队,无返回值,仅删除) 
    q.pop(); 
    // 删除10,队头变为20 
    cout << "出队后队头元素:" << q.front() << endl; 
    // 输出20 
    // 5. 获取元素个数:size(),O(1) 
    cout << "队列内元素个数:" << q.size() << endl; 
    // 输出2 
    // 6. 判断队列是否为空:empty(),O(1) 
    cout << "队列是否为空:" << q.empty() << endl; 
    // 输出0(false) 
    // 7. 清空队列(queue无clear()函数,需通过循环pop()实现) 
    while (!q.empty()) 
    { q.pop(); } 
    cout << "清空后队列是否为空:" << q.empty() << endl; 
    // 输出1(true) return 0; 
}

输出结果

注意:

  1. pop()函数无返回值 ,仅删除队头元素,若需要获取队头元素,需先调用front()查看,再调用pop()删除。

  2. queue没有clear()成员函数,清空队列需通过循环调用pop(),直到empty()返回true。

  3. 当队列为空时,调用front()back()pop()会触发未定义行为,操作前务必用empty()判断。

六、常见误区与注意事项

  1. 混淆「容器适配器」与「原生序列容器」:stack和queue是适配器,不是原生容器,它们封装了底层容器(deque/vector/list),仅暴露有限的接口,不支持迭代器和随机访问;而deque是原生序列容器,接口更丰富,支持双端操作和随机访问。

  2. stack/queue的遍历问题:两者均无迭代器,无法直接遍历,若需查看所有元素,只能通过「获取头部/栈顶元素→删除元素」的循环方式,且该方式会清空容器内的元素(若需保留数据,可先拷贝一份容器)。

  3. deque与vector、list的选择

    1. 需频繁随机访问、插入/删除多在尾部 → 选择vector;

    2. 需频繁在中间插入/删除、无需随机访问 → 选择list;

    3. 需频繁在两端插入/删除、偶尔随机访问 → 选择deque。

  4. stack/queue的底层容器替换:stack可指定vector/list为底层容器,queue可指定list为底层容器,但不推荐为queue指定vector(vector头部删除效率低,违背queue的设计初衷)。

七、实战案例:三者的典型应用

案例1:stack实现括号匹配(判断字符串中的括号是否成对有效)

cpp 复制代码
#include <stack> 
#include <iostream> 
#include <string> 
using namespace std; 
// 判断括号是否有效匹配
bool isValidBracket(const string& s) 
{ 
    stack<char> st; 
    for (char c : s) 
    { 
        // 左括号入栈 
        if (c == '(' || c == '[' || c == '{') 
        { st.push(c); } 
        else 
        {    
            // 右括号:栈为空则无匹配的左括号,直接返回false 
            if (st.empty()) 
            { return false; } 
            // 取出栈顶左括号,判断是否与当前右括号匹配 
            char topChar = st.top(); 
            st.pop(); 
            if ((c == ')' && topChar != '(') || (c == ']' 
                && topChar != '[') || (c == '}' && topChar != '{')) 
            { return false; } 
        } 
    } 
    // 遍历结束后,栈为空说明所有括号都匹配完成 
    return st.empty(); 
} 

int main() 
{ 
    string s1 = "()[]{}"; 
    string s2 = "([)]"; 
    string s3 = "{[]}"; 
    cout << s1 << " 是否有效:" << (isValidBracket(s1) ? "是" : "否") << endl;
    cout << s2 << " 是否有效:" << (isValidBracket(s2) ? "是" : "否") << endl;
    cout << s3 << " 是否有效:" << (isValidBracket(s3) ? "是" : "否") << endl; 
    return 0; 
}

输出结果

案例2:queue实现简单消息队列(先进先出处理消息)

cpp 复制代码
#include <queue> 
#include <iostream> 
#include <string> 
using namespace std; 
// 简单消息队列类 
class MessageQueue 
{ 
private: queue<string> msgQueue; // 存储消息的queue 
public: 
	// 发送消息(队尾入队) 
	void sendMsg(const string& msg) 
	{ 
		msgQueue.push(msg); 
		cout << "发送消息:" << msg << endl; 
	} 
	// 接收消息(队头出队)
	void recvMsg() 
	{ 
		if (msgQueue.empty()) 
		{ 
			cout << "无未读消息!" << endl; 
			return; 
		} 
		string msg = msgQueue.front(); 
		msgQueue.pop(); 
		cout << "接收消息:" << msg << endl; 
	} 
	// 查看未读消息数量 
	int getUnreadCount() const 
	{ 
		return msgQueue.size(); 
	} 
}; 

int main() 
{
	MessageQueue mq;
	mq.sendMsg("Hello, C++ STL!"); 
	mq.sendMsg("这是一个队列消息");
	mq.sendMsg("消息处理完成~");
	cout << "未读消息数量:" << mq.getUnreadCount() << endl; 
	mq.recvMsg(); 
	mq.recvMsg(); 
	mq.recvMsg(); 
	mq.recvMsg();
	return 0; 
}

输出结果

案例3:deque实现滑动窗口(输出数组中每个窗口的最大值,简化版)

cpp 复制代码
#include <deque> 
#include <iostream>
#include <vector>
using namespace std;
// 滑动窗口:窗口大小为k,输出每个窗口的最大值(简化版) 
void slidingWindowMax(const vector<int>& nums, int k) 
{ 
	deque<int> dq; 
	// 存储数组下标,维护窗口内的最大值下标 
	for (int i = 0; i < nums.size(); i++) 
	{ 
		// 1. 移除超出窗口范围的下标 
		if (!dq.empty() && dq.front() < i - k + 1) 
		{ dq.pop_front(); }
		// 2. 移除deque中比当前元素小的下标,保证deque头部是窗口最大值下标
		while (!dq.empty() && nums[dq.back()] < nums[i]) 
		{ dq.pop_back(); } 
		// 3. 当前下标入队 
		dq.push_back(i); 
		// 4. 窗口大小达到k时,输出最大值 
		if (i >= k - 1) 
		{ 
			cout << "窗口[" << i - k + 1 << "," << i 
			<< "] 最大值:" << nums[dq.front()] << endl; 
		} 
	} 
} 
int main() 
{
	vector<int> nums = {1,3,-1,-3,5,3,6,7};
	int k = 3; 
	slidingWindowMax(nums, k);
	return 0; 
}

输出结果

八、三者核心特性对比表格

为了方便大家快速梳理和记忆,整理了stack、queue、deque的核心特性对比:

特性 stack(栈) queue(队列) deque(双端队列)
数据结构类型 容器适配器 容器适配器 原生序列容器
访问规则 后进先出(LIFO),仅栈顶操作 先进先出(FIFO),队尾入、队头出 双端操作+随机访问
底层默认容器 deque deque -(自身为底层容器)
支持的核心操作位置 栈顶 队头(删、查)、队尾(插、查) 头部、尾部、中间(随机访问)
迭代器支持 不支持 不支持 支持随机访问迭代器
核心成员函数 push()、pop()、top() push()、pop()、front()、back() push_front/back()、pop_front/back()、[]、at()
清空方式 循环pop() 循环pop() clear()(直接支持)
时间复杂度(核心操作) O(1) O(1) 双端操作O(1),随机访问O(1)
典型适用场景 括号匹配、函数调用栈 任务队列、BFS、排队模拟 滑动窗口、缓存队列、双端操作场景

总结

  1. deque 是原生双端队列,支持双端高效操作和随机访问,是 stack、queue 的默认底层容器,适合需要两端操作且偶尔随机访问的场景。

  2. stack 是后进先出适配器,仅支持栈顶操作,无迭代器,适合括号匹配、逆序输出等 LIFO 逻辑场景。

  3. queue 是先进先出适配器,支持队尾入、队头出,无迭代器,适合任务队列、BFS 等 FIFO 逻辑场景。

  4. 容器适配器(stack/queue)无 clear() 方法,需循环 pop() 清空,操作前务必用 empty() 判断是否为空,避免未定义行为。

对于C++初学者而言,掌握这三个容器的核心用法,能灵活应对多数线性数据结构的应用场景,后续可结合更多实战案例,深化对STL容器的理解与运用。

相关推荐
Ronaldinho Gaúch4 小时前
leetcode279完全平方数
c++·算法·动态规划
王老师青少年编程4 小时前
2024信奥赛C++提高组csp-s复赛真题及题解:超速检测
c++·真题·csp·信奥赛·csp-s·提高组·超速检测
Howrun7774 小时前
C++_bind_可调用对象转化器
开发语言·c++·算法
爱装代码的小瓶子5 小时前
【C++与Linux基础】文件篇(3)-fd的本质和minishell的重定向功能
linux·c++
2301_822382765 小时前
嵌入式C++实时内核
开发语言·c++·算法
Max_uuc5 小时前
【C++ 硬核】拒绝单位混淆:利用 Phantom Types (幻影类型) 实现零开销的物理量安全计算
开发语言·c++
2301_790300965 小时前
C++与物联网开发
开发语言·c++·算法
凤年徐5 小时前
C++ STL list 容器详解:使用与模拟实现
开发语言·c++·后端·list
艾莉丝努力练剑5 小时前
【Linux进程控制(三)】实现自主Shell命令行解释器
linux·运维·服务器·c++·人工智能·安全·云原生