从零开始的C++学习生活 9:stack_queue的入门使用和模板进阶

个人主页:Yupureki-CSDN博客

C++专栏:C++_Yupureki的博客-CSDN博客

目录

stack和queue

前言

[1. stack的基本使用](#1. stack的基本使用)

[1.1 栈的基本概念](#1.1 栈的基本概念)

[1.2 stack的基本使用](#1.2 stack的基本使用)

[1.3 栈的模拟实现](#1.3 栈的模拟实现)

[2. queue的基本使用](#2. queue的基本使用)

[2.1 队列的基本概念](#2.1 队列的基本概念)

[2.2 queue的基本使用](#2.2 queue的基本使用)

[2.3 队列的模拟实现](#2.3 队列的模拟实现)

[3. priority_queue(优先队列)深度解析](#3. priority_queue(优先队列)深度解析)

[3.1 优先队列的概念](#3.1 优先队列的概念)

[3.2 priority_queue的使用](#3.2 priority_queue的使用)

[4. 容器适配器深度理解](#4. 容器适配器深度理解)

[4.1 什么是容器适配器?](#4.1 什么是容器适配器?)

[4.2 为什么选择deque作为默认底层容器?](#4.2 为什么选择deque作为默认底层容器?)

结语

模板进阶

前言

[5. 非类型模板参数](#5. 非类型模板参数)

[5.1 基本概念](#5.1 基本概念)

[5.2 非类型参数的限制](#5.2 非类型参数的限制)

[6. 模板特化](#6. 模板特化)

[6.1 为什么需要模板特化?](#6.1 为什么需要模板特化?)

[6.2 函数模板特化](#6.2 函数模板特化)

[6.3 类模板特化](#6.3 类模板特化)

[6.3.1 全特化(Full Specialization)](#6.3.1 全特化(Full Specialization))

[6.3.2 偏特化(Partial Specialization)](#6.3.2 偏特化(Partial Specialization))

[7. 模板分离编译](#7. 模板分离编译)

[7.1 问题背景](#7.1 问题背景)

[7.2 问题原因分析](#7.2 问题原因分析)

[7.3 解决方案](#7.3 解决方案)

方案1:声明和定义放在同一文件(推荐)

方案2:显式实例化(不推荐)

结语


上一篇:从零开始的C++学习生活 8:list的入门使用-CSDN博客

stack和queue

前言

在C++标准模板库(STL)中,stack(栈)和queue(队列)作为两种经典的线性数据结构,虽然功能相对简单,却在算法设计和系统开发中扮演着不可或缺的角色。与vectorlist等直接容器不同,它们属于容器适配器------通过封装其他容器来实现特定接口。

栈的"后进先出"(LIFO)和队列的"先进先出"(FIFO)特性,使得它们在解决特定类型问题时表现出色。无论是函数调用栈、表达式求值,还是任务调度、消息队列,都能看到它们的身影。

本文将深入探讨stackqueue的实现原理、使用技巧以及实际应用,帮助你全面掌握这两种重要的数据结构。

1. stack的基本使用

1.1 栈的基本概念

栈是一种后进先出(LIFO)的数据结构,只允许在容器的一端进行插入和删除操作。这就像一叠盘子,我们只能从最上面取放。

栈的核心操作:

  • push(): 元素入栈

  • pop(): 元素出栈

  • top(): 查看栈顶元素

  • empty(): 判断栈是否为空

  • size(): 获取栈中元素个数

1.2 stack的基本使用

stack相对于之前的string,vector和list,结构比较简单

cpp 复制代码
#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;  // 3
    cout << "栈顶元素: " << 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即为一个容器适配器,之后我们会初步讲解

cpp 复制代码
#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 1
        st.pop();
    }
}

2. queue的基本使用

2.1 队列的基本概念

队列是一种先进先出(FIFO)的数据结构,元素从队尾进入,从队头离开。这就像现实生活中的排队,先来的人先接受服务。

队列的核心操作:

  • push(): 元素入队

  • pop(): 元素出队

  • front(): 查看队头元素

  • back(): 查看队尾元素

  • empty(): 判断队列是否为空

  • size(): 获取队列元素个数

2.2 queue的基本使用

cpp 复制代码
#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;    // 3
    cout << "队头元素: " << q.front() << endl;   // 1
    cout << "队尾元素: " << q.back() << endl;    // 3
    
    // 出队操作
    q.pop();
    cout << "出队后队头: " << q.front() << endl; // 2
    
    // 遍历队列
    while (!q.empty()) {
        cout << q.front() << " ";
        q.pop();
    }
    // 输出: 2 3
}

2.3 队列的模拟实现

cpp 复制代码
#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的使用

cpp 复制代码
#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作为默认底层容器?

cpp 复制代码
// 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的原因:

  1. 综合性能优秀

    • 头部尾部操作都是O(1)

    • 不需要vector的扩容拷贝开销

    • 比list缓存友好,内存局部性更好

  2. 适合适配器需求

    • stack只需要push_backpop_backback

    • queue需要push_backpop_frontfrontback

    • deque完美支持这些操作

  3. 内存效率

    • 分段连续存储,扩容代价小

    • 空间利用率高于list

结语

stackqueue作为C++ STL中的容器适配器,虽然接口简单,却在算法设计和系统开发中发挥着重要作用。通过本文的学习,我们应该:

  • 理解栈和队列的基本特性和操作

  • 掌握容器适配器的设计思想

  • 熟悉优先队列的原理和使用

  • 能够根据场景选择合适的容器

  • 理解底层容器的选择策略

在实际开发中,当遇到具有LIFO或FIFO特性的问题时,不要忘记这些简单而强大的工具。它们往往能以最优雅的方式解决看似复杂的问题。

模板的上一篇:从零开始的C++学习生活 5:内存管理和模板初阶-CSDN博客

模板进阶

前言

在前一篇文章中,我们学习了模板的基础知识,了解了函数模板和类模板的基本用法。但在实际开发中,我们常常会遇到更复杂的场景:需要固定大小的容器、针对特定类型进行特殊处理、或者将模板代码分文件组织等。

C++模板系统提供了强大的进阶特性来解决这些问题,包括非类型模板参数、模板特化、以及处理分离编译的策略。这些特性让我们能够编写更加灵活、高效的泛型代码。

5. 非类型模板参数

5.1 基本概念

非类型模板参数允许我们使用常量作为模板参数,而不仅仅是类型。这使得我们可以在编译期确定某些值,生成更加特化的代码。

cpp 复制代码
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. 非类型的模板参数必须在编译期就能确认结果。

cpp 复制代码
// 允许的类型:
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 为什么需要模板特化?

假设我们实现一个判断是否相等的函数模板

cpp 复制代码
// 通用模板
template<typename T>
bool equals(const T& a, const T& b) {
    return a == b;
}

模板提供了通用实现,但某些特定类型可能需要特殊处理:

对于浮点数,我们需要考虑精度问题

cpp 复制代码
template<>
bool equals<double>(const double& a, const double& b) {
    return std::abs(a - b) < 1e-10;
}

对于字符串,我们需利用strcmp来比较大小

cpp 复制代码
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. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

cpp 复制代码
// 基础函数模板
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;
}

注意:函数模板特化在实际开发中较少使用,通常更推荐使用函数重载:

cpp 复制代码
// 使用重载代替特化(更简单清晰)
int compare(const char* a, const char* b) {
    return std::strcmp(a, b);
}

6.3 类模板特化

6.3.1 全特化(Full Specialization)

全特化是指为模板的所有参数都提供具体类型:

cpp 复制代码
// 通用类模板
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)

偏特化是指只特化部分模板参数,或者对参数类型添加约束:

部分参数特化:

cpp 复制代码
// 通用模板
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;   // 第二个偏特化
}

类型约束特化:

如果模板参数被特例化为指针引用,那么就会强制性调用含指针或引用的模板

cpp 复制代码
//两个参数偏特化为指针类型 
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

cpp 复制代码
// math_utils.h
template<typename T>
T add(const T& a, const T& b);
cpp 复制代码
// math_utils.cpp
template<typename T>
T add(const T& a, const T& b) {
    return a + b;
}
cpp 复制代码
// main.cpp
#include "math_utils.h"

int main() {
    int result = add(1, 2);  // 链接错误:undefined reference
    return 0;
}

7.2 问题原因分析

编译过程:

  1. 编译math_utils.cpp:编译器看到模板定义,但没有看到具体实例化,不会生成代码

  2. 编译main.cpp :编译器看到模板声明,生成对add<int>的调用

  3. 链接阶段 :找不到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:声明和定义放在同一文件(推荐)
cpp 复制代码
// math_utils.h
template<typename T>
T add(const T& a, const T& b) {
    return a + b;
}

// 或者使用.hpp后缀明确表示这是包含实现的头文件
// math_utils.hpp
方案2:显式实例化(不推荐)

结语

通过本文的学习,我们深入探讨了C++模板的进阶特性:

  • 非类型模板参数:在编译期确定值,实现固定大小的容器和编译期计算

  • 模板特化:为特定类型提供特殊实现,包括全特化和偏特化

  • 分离编译问题:理解模板的编译模型,掌握正确的代码组织方式

这些进阶特性让我们能够编写更加高效、灵活的泛型代码。但需要注意的是,强大的能力也带来了复杂性,在实际项目中应该:

  • 优先使用简单的模板特性

  • 只在性能关键路径使用高级特性

  • 保持代码的可读性和可维护性

  • 充分测试各种特化版本

相关推荐
远远远远子4 小时前
C++-- 内存管理
c++·算法
一念&4 小时前
每日一个C语言知识:C 数组
c语言·开发语言·算法
小年糕是糕手4 小时前
【数据结构】单链表“0”基础知识讲解 + 实战演练
c语言·开发语言·数据结构·c++·学习·算法·链表
疯狂吧小飞牛5 小时前
Lua C API 中的 lua_rawseti 与 lua_rawgeti 介绍
c语言·开发语言·lua
半夏知半秋5 小时前
lua对象池管理工具剖析
服务器·开发语言·后端·学习·lua
Dobby_055 小时前
【Go】C++ 转 Go 第(一)天:环境搭建 Windows + VSCode 远程连接 Linux
linux·运维·c++·vscode·golang
咸鱼爱学习5 小时前
【题解】B2613【深基1.习5】打字速度
数据结构·c++·算法
一匹电信狗5 小时前
【C++】C++风格的类型转换
服务器·开发语言·c++·leetcode·小程序·stl·visual studio
阿林学习计算机5 小时前
AVL树的实现
数据结构