从零开始的C++学习生活 9:stack_queue的入门使用和模板进阶
个人主页:Yupureki-CSDN博客
C++专栏:C++_Yupureki的博客-CSDN博客
目录
stack和queue
前言
1. stack的基本使用
1.1 栈的基本概念
1.2 stack的基本使用
1.3 栈的模拟实现
2. queue的基本使用
2.1 队列的基本概念
2.2 queue的基本使用
2.3 队列的模拟实现
3. priority_queue(优先队列)深度解析
3.1 优先队列的概念
3.2 priority_queue的使用
4. 容器适配器深度理解
4.1 什么是容器适配器?
4.2 为什么选择deque作为默认底层容器?
结语
模板进阶
前言
5. 非类型模板参数
5.1 基本概念
5.2 非类型参数的限制
6. 模板特化
6.1 为什么需要模板特化?
6.2 函数模板特化
6.3 类模板特化
6.3.1 全特化(Full Specialization)
6.3.2 偏特化(Partial Specialization)
7. 模板分离编译
7.1 问题背景
7.2 问题原因分析
7.3 解决方案
方案1:声明和定义放在同一文件(推荐)
方案2:显式实例化(不推荐)
结语
上一篇:从零开始的C++学习生活 8:list的入门使用-CSDN博客
stack和queue
前言
在C++标准模板库(STL)中,stack
(栈)和queue
(队列)作为两种经典的线性数据结构,虽然功能相对简单,却在算法设计和系统开发中扮演着不可或缺的角色。与vector
、list
等直接容器不同,它们属于容器适配器——通过封装其他容器来实现特定接口。
栈的"后进先出"(LIFO)和队列的"先进先出"(FIFO)特性,使得它们在解决特定类型问题时表现出色。无论是函数调用栈、表达式求值,还是任务调度、消息队列,都能看到它们的身影。
本文将深入探讨stack
和queue
的实现原理、使用技巧以及实际应用,帮助你全面掌握这两种重要的数据结构。
1. stack的基本使用
1.1 栈的基本概念
栈是一种后进先出(LIFO)的数据结构,只允许在容器的一端进行插入和删除操作。这就像一叠盘子,我们只能从最上面取放。
栈的核心操作:
-
push()
: 元素入栈 -
pop()
: 元素出栈 -
top()
: 查看栈顶元素 -
empty()
: 判断栈是否为空 -
size()
: 获取栈中元素个数
1.2 stack的基本使用
stack相对于之前的string,vector和list,结构比较简单
#include <stack>
#include <iostream>
using namespace std;void basicStackDemo() {stack<int> st;// 入栈操作st.push(1);st.push(2);st.push(3);cout << "栈大小: " << st.size() << endl; // 3cout << "栈顶元素: " << st.top() << endl; // 3// 出栈操作st.pop();cout << "出栈后栈顶: " << st.top() << endl; // 2// 遍历栈(注意:栈没有迭代器,只能边pop边访问)while (!st.empty()) {cout << st.top() << " ";st.pop();}// 输出: 2 1
}
1.3 栈的模拟实现
由于栈比较简单,为了方便,我们可以调用其他的STL容器的功能函数来帮我们实现,下列的deque即为一个容器适配器,之后我们会初步讲解
#include <vector>
#include <deque>namespace my {template<class T, class Container = std::deque<T>>class stack {private:Container _c; // 底层容器public:// 构造函数stack() = default;// 容量操作bool empty() const { return _c.empty(); }size_t size() const { return _c.size(); }// 元素访问T& top() { return _c.back(); }const T& top() const { return _c.back(); }// 修改操作void push(const T& value) { _c.push_back(value); }void pop() { _c.pop_back(); }// 交换void swap(stack& other) { std::swap(_c, other._c); }};
}// 使用示例
void testMyStack() {my::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()) {cout << st.top() << " "; // 3 2 1st.pop();}
}
2. queue的基本使用
2.1 队列的基本概念
队列是一种先进先出(FIFO)的数据结构,元素从队尾进入,从队头离开。这就像现实生活中的排队,先来的人先接受服务。
队列的核心操作:
-
push()
: 元素入队 -
pop()
: 元素出队 -
front()
: 查看队头元素 -
back()
: 查看队尾元素 -
empty()
: 判断队列是否为空 -
size()
: 获取队列元素个数
2.2 queue的基本使用
#include <queue>
#include <iostream>
using namespace std;void basicQueueDemo() {queue<int> q;// 入队操作q.push(1);q.push(2);q.push(3);cout << "队列大小: " << q.size() << endl; // 3cout << "队头元素: " << q.front() << endl; // 1cout << "队尾元素: " << q.back() << endl; // 3// 出队操作q.pop();cout << "出队后队头: " << q.front() << endl; // 2// 遍历队列while (!q.empty()) {cout << q.front() << " ";q.pop();}// 输出: 2 3
}
2.3 队列的模拟实现
#include <list>namespace my {template<class T, class Container = std::list<T>>class queue {private:Container _c;public:queue() = default;// 容量操作bool empty() const { return _c.empty(); }size_t size() const { return _c.size(); }// 元素访问T& front() { return _c.front(); }const T& front() const { return _c.front(); }T& back() { return _c.back(); }const T& back() const { return _c.back(); }// 修改操作void push(const T& value) { _c.push_back(value); }void pop() { _c.pop_front(); }void swap(queue& other) { std::swap(_c, other._c); }};
}
3. priority_queue(优先队列)深度解析
3.1 优先队列的概念
优先队列是一种特殊的队列,元素出队的顺序不是按照入队顺序,而是按照元素的优先级。默认情况下,C++中的priority_queue
是大顶堆。
因此priority_queue的底层结构为堆
由于priority_queue仍为队列,因此仍具有以下操作
empty():检测容器是否为空
size():返回容器中有效元素个数
front():返回容器中第一个元素的引用
push_back():在容器尾部插入元素
pop_back():删除容器尾部元素
标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue 类实例化指定容器类,则使用vector。
3.2 priority_queue的使用
#include <queue>
#include <vector>
#include <functional>void priorityQueueDemo() {// 默认大顶堆priority_queue<int> max_heap;max_heap.push(3);max_heap.push(1);max_heap.push(4);max_heap.push(2);cout << "大顶堆顶部: " << max_heap.top() << endl; // 4// 小顶堆priority_queue<int, vector<int>, greater<int>> min_heap;min_heap.push(3);min_heap.push(1);min_heap.push(4);min_heap.push(2);cout << "小顶堆顶部: " << min_heap.top() << endl; // 1
}
4. 容器适配器深度理解
4.1 什么是容器适配器?
容器适配器不是独立的容器,而是对现有容器的封装,提供特定的接口。可以简单理解为容器适配器是对于一个容器的特定功能的实现
例如stack实现先进后出,queue实现先进先出
而vector和list并不具有这些特征
STL中的容器适配器包括:
-
stack
: 栈适配器 -
queue
: 队列适配器 -
priority_queue
: 优先队列适配器
其中stack和queue均使用的是deque
4.2 为什么选择deque作为默认底层容器?
// STL中的默认定义
template <class T, class Container = deque<T>> class stack;
template <class T, class Container = deque<T>> class queue;
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端 进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与 list比较,空间利用率比较高。
deque先是由一个中控台(map)构成,每个中控台节点包含cur,first,last,node四个指针
first代表一个数组的首地址
last代表一个数组的尾地址
cur则用来遍历数组中的元素
node代表该中控台节点的位置
因此deque结合了list和vector的优点,相当于几个数组以链表的形式连接起来
选择deque的原因:
-
综合性能优秀:
-
头部尾部操作都是O(1)
-
不需要vector的扩容拷贝开销
-
比list缓存友好,内存局部性更好
-
-
适合适配器需求:
-
stack只需要
push_back
、pop_back
、back
-
queue需要
push_back
、pop_front
、front
、back
-
deque完美支持这些操作
-
-
内存效率:
-
分段连续存储,扩容代价小
-
空间利用率高于list
-
结语
stack
和queue
作为C++ STL中的容器适配器,虽然接口简单,却在算法设计和系统开发中发挥着重要作用。通过本文的学习,我们应该:
-
理解栈和队列的基本特性和操作
-
掌握容器适配器的设计思想
-
熟悉优先队列的原理和使用
-
能够根据场景选择合适的容器
-
理解底层容器的选择策略
在实际开发中,当遇到具有LIFO或FIFO特性的问题时,不要忘记这些简单而强大的工具。它们往往能以最优雅的方式解决看似复杂的问题。
模板的上一篇:从零开始的C++学习生活 5:内存管理和模板初阶-CSDN博客
模板进阶
前言
在前一篇文章中,我们学习了模板的基础知识,了解了函数模板和类模板的基本用法。但在实际开发中,我们常常会遇到更复杂的场景:需要固定大小的容器、针对特定类型进行特殊处理、或者将模板代码分文件组织等。
C++模板系统提供了强大的进阶特性来解决这些问题,包括非类型模板参数、模板特化、以及处理分离编译的策略。这些特性让我们能够编写更加灵活、高效的泛型代码。
5. 非类型模板参数
5.1 基本概念
非类型模板参数允许我们使用常量作为模板参数,而不仅仅是类型。这使得我们可以在编译期确定某些值,生成更加特化的代码。
template<class T, size_t N = 10> // N是非类型模板参数
class Array {
private:T _data[N]; // 使用N作为数组大小size_t _size = N;public:size_t size() const { return _size; }T& operator[](size_t index) { return _data[index]; }const T& operator[](size_t index) const { return _data[index]; }
};// 使用示例
void demo() {Array<int, 5> arr1; // 创建大小为5的int数组Array<double, 10> arr2; // 创建大小为10的double数组Array<char> arr3; // 使用默认大小10
}
5.2 非类型参数的限制
注意:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。
// 允许的类型:
template<int N> class A {}; // 整型
template<size_t N> class B {}; // 无符号整型
template<bool Flag> class C {}; // 布尔类型
template<int* Ptr> class D {}; // 指针类型
template<int (&Func)()> class E {}; // 函数引用// 不允许的类型:
// template<double Value> class F {}; // 错误:浮点数不允许
// template<std::string Str> class G {}; // 错误:类对象不允许
// template<const char* Str> class H {}; // 错误:字符串字面量不允许
6. 模板特化
6.1 为什么需要模板特化?
假设我们实现一个判断是否相等的函数模板
// 通用模板
template<typename T>
bool equals(const T& a, const T& b) {return a == b;
}
模板提供了通用实现,但某些特定类型可能需要特殊处理:
对于浮点数,我们需要考虑精度问题
template<>
bool equals<double>(const double& a, const double& b) {return std::abs(a - b) < 1e-10;
}
对于字符串,我们需利用strcmp来比较大小
template<>
bool equals<const char*>(const char* const& a, const char* const& b) {return std::strcmp(a, b) == 0;
}
可以看到equals对于一些基本数据类型,如整型,字符的判断无误,但若是字符串甚至是自定义类型对比则不能草率地使用==来判断
因此对于这些特殊情况,我们需要以模板来专门实现几个函数
6.2 函数模板特化
函数模板特化允许我们为特定类型提供特殊实现:
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
// 基础函数模板
template<typename T>
int compare(const T& a, const T& b) {if (a < b) return -1;if (b < a) return 1;return 0;
}// 特化版本:针对C风格字符串
template<>
int compare<const char*>(const char* const& a, const char* const& b) {return std::strcmp(a, b);
}// 特化版本:针对double(处理浮点精度)
template<>
int compare<double>(const double& a, const double& b) {if (std::abs(a - b) < 1e-10) return 0;return a < b ? -1 : 1;
}
注意:函数模板特化在实际开发中较少使用,通常更推荐使用函数重载:
// 使用重载代替特化(更简单清晰)
int compare(const char* a, const char* b) {return std::strcmp(a, b);
}
6.3 类模板特化
6.3.1 全特化(Full Specialization)
全特化是指为模板的所有参数都提供具体类型:
// 通用类模板
template<typename T1, typename T2>
class Pair {
private:T1 _first;T2 _second;public:Pair(const T1& f, const T2& s) : _first(f), _second(s) {}void print() const {std::cout << "Generic: (" << _first << ", " << _second << ")" << std::endl;}
};// 全特化:两个参数都是int
template<>
class Pair<int, int> {
private:int _first;int _second;public:Pair(int f, int s) : _first(f), _second(s) {}void print() const {std::cout << "IntPair: (" << _first << ", " << _second << ")" << std::endl;}// 特化版本特有的方法int sum() const { return _first + _second; }
};void demoFullSpecialization() {Pair<double, std::string> p1(3.14, "pi");Pair<int, int> p2(10, 20);p1.print(); // 输出: Generic: (3.14, pi)p2.print(); // 输出: IntPair: (10, 20)std::cout << "Sum: " << p2.sum() << std::endl; // 输出: Sum: 30
}
6.3.2 偏特化(Partial Specialization)
偏特化是指只特化部分模板参数,或者对参数类型添加约束:
部分参数特化:
// 通用模板
template<typename T1, typename T2, typename T3>
class Triple {
public:Triple() { std::cout << "Triple<T1, T2, T3>" << std::endl; }
};// 偏特化:第三个参数固定为int
template<typename T1, typename T2>
class Triple<T1, T2, int> {
public:Triple() { std::cout << "Triple<T1, T2, int>" << std::endl; }
};// 偏特化:第二、三个参数固定
template<typename T1>
class Triple<T1, double, int> {
public:Triple() { std::cout << "Triple<T1, double, int>" << std::endl; }
};void demoPartialSpecialization() {Triple<float, char, bool> t1; // 通用版本Triple<float, char, int> t2; // 第一个偏特化Triple<float, double, int> t3; // 第二个偏特化
}
类型约束特化:
如果模板参数被特例化为指针或引用,那么就会强制性调用含指针或引用的模板
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:T1 _d1;T2 _d2;};//两个参数偏特化为引用类型
template <typename T1, typename T2>class Data <T1&, T2&>{public:Data(const T1& d1, const T2& d2): _d1(d1), _d2(d2){cout<<"Data<T1&, T2&>" <<endl;}private:const T1 & _d1;const T2 & _d2;
};void test2 () {Data<double , int> d1; // 调用特化的int版本Data<int , double> d2; // 调用基础的模板 Data<int *, int*> d3; // 调用特化的指针版本Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}
7. 模板分离编译
7.1 问题背景
当模板的声明和定义分离到不同文件时,会出现链接错误:
假设我们有一个头文件math_utils.h和两个原文件math_utils.cpp和main.cpp
// math_utils.h
template<typename T>
T add(const T& a, const T& b);
// math_utils.cpp
template<typename T>
T add(const T& a, const T& b) {return a + b;
}
// main.cpp
#include "math_utils.h"int main() {int result = add(1, 2); // 链接错误:undefined referencereturn 0;
}
7.2 问题原因分析
编译过程:
-
编译math_utils.cpp:编译器看到模板定义,但没有看到具体实例化,不会生成代码
-
编译main.cpp:编译器看到模板声明,生成对
add<int>
的调用 -
链接阶段:找不到
add<int>
的实现,链接失败
原因:
模板之所以叫模板,就只是一张图纸,只有你需要才会构造出相应的函数实例
.h中的模板add为声明,编译器看见了因此main中的add不会报错,但当实际调用中无法找到add函数,但是math_utils.cpp中存在啊?实际上不存在,因为math_utils.cpp中的add函数为模板,在链接过程中add函数模板并没有生成相应的实例,因为math_utils.cpp中没有调用add函数,main.cpp无法远程在math_utils.cpp中用add模板,不然也没有链接这个过程
7.3 解决方案
方案1:声明和定义放在同一文件(推荐)
// math_utils.h
template<typename T>
T add(const T& a, const T& b) {return a + b;
}// 或者使用.hpp后缀明确表示这是包含实现的头文件
// math_utils.hpp
方案2:显式实例化(不推荐)
结语
通过本文的学习,我们深入探讨了C++模板的进阶特性:
-
非类型模板参数:在编译期确定值,实现固定大小的容器和编译期计算
-
模板特化:为特定类型提供特殊实现,包括全特化和偏特化
-
分离编译问题:理解模板的编译模型,掌握正确的代码组织方式
这些进阶特性让我们能够编写更加高效、灵活的泛型代码。但需要注意的是,强大的能力也带来了复杂性,在实际项目中应该:
-
优先使用简单的模板特性
-
只在性能关键路径使用高级特性
-
保持代码的可读性和可维护性
-
充分测试各种特化版本