《C++初阶之STL》【泛型编程 + STL简介】

发布于:2025-07-15 ⋅ 阅读:(14) ⋅ 点赞:(0)

在这里插入图片描述

往期《C++初阶》回顾:

/------------ 入门基础 ------------/
【C++的前世今生】
【命名空间 + 输入&输出 + 缺省参数 + 函数重载】
【普通引用 + 常量引用 + 内联函数 + nullptr】
/------------ 类和对象 ------------/
【类 + 类域 + 访问限定符 + 对象的大小 + this指针】
【类的六大默认成员函数】
【初始化列表 + 自定义类型转换 + static成员】
【友元 + 内部类 + 匿名对象】
【经典案例:日期类】
/------------ 内存管理------------/
【内存分布 + operator new/delete + 定位new】

前言:

hi(。・∀・)ノ゙嗨,假期里新的一周又要开始了,感慨一下天气是真的热啊🌞,但是时间不等人,反过来想我们应该庆幸天气还热,假期还长⏳。
只有我们在最热的时候选择了坚持,那么当暑气褪去之时,我们才不会在满是收获的金秋里觉得秋意是这么的凄凉。

那么从今天起,咱们就一头扎进 C++ 核心 STL 库 的奇妙世界啦 (≧∇≦)ノ !首节内容聚焦 【泛型编程 + STL 简介】 ,这可是后续深入学习 STL 的基石呀✨ 。
把泛型思想吃透,弄懂 STL 整体框架,往后学容器、算法、迭代器这些,就像走在铺好路的大道上,顺顺当当、一路生花啦~ 🚀

---------------泛型编程 ---------------

什么是泛型编程?

泛型编程:这种风格以 模板 为中心,将算法和数据结构抽象为与具体类型无关的通用形式,通过参数化类型实现代码复用,使程序在保持高效的同时兼具灵活性。


核心思想:

  • 抽象类型:将算法和数据结构设计为 “通用模板”,不依赖特定数据类型。
  • 编译时实例化:在使用时通过模板参数指定具体类型,编译器自动生成对应代码。
  • 类型安全:在编译阶段检查类型匹配,避免运行时错误。

为什么要引入泛型编程?

请小伙伴们试想一下下面的这种场景:
假设你正在负责一个大型项目,其中需要实现三种不同类型变量的交换功能:整数、浮点数和字符的交换功能。
这时候你可能会说,这个问题并不复杂呀~,我们只需要实现三个形参是不同类型的的Swap()函数即可,因为这三个函数会构成重载,之后会根据我们所传的不同的参数而调用不同的函数。


哈哈不错,你想到的这种实现方式很直观,但是呢存在两个明显的缺陷:代码冗余难以维护

  • 代码冗余:体现在每增加一种新的数据类型,就需要复制粘贴几乎相同的代码,这不仅违反了软件开发中的 DRY(Don't Repeat Yourself)原则,还会使代码库膨胀,导致编译与运行时开销增加。

  • 难以维护:更糟糕的是,如果后续需要修改交换逻辑,就必须同时修改所有重载函数,很容易遗漏某个版本而导致 bug

void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

void Swap(double& left, double& right)
{
    double temp = left;
    left = right;
    right = temp;
}

void Swap(char& left, char& right)
{
    char temp = left;
    left = right;
    right = temp;
}

我想此时小伙伴们一定在想:“那么,有没有一种更优雅的解决方案,既能保持代码的简洁性,又能避免重复劳动呢?”

哈哈,小伙伴现在面临的这个问题,早在上个世纪的C++开发者也同样面临过这个问题,

当时的前辈是这么想的:“在 C++ 中,要是能有这么一个 “模具” 就好了:只需往里面填入不同 “材料”(也就是类型),就能铸造出不同材质的 “零件”(生成对应类型的代码)。这样一来,程序员们就能少掉不少头发啦~。”,于是前辈们为我们种下了这棵泛型编程的大树,让我们得以在此乘凉。


所以说:上面的这种场景这正是 泛型编程(Generic Programming) 发挥作用的场景 —— 通过 C++ 的 模板(Template) 机制,我们可以定义一个通用的Swap()函数,它能够自动适应任何数据类型,而无需为每种类型单独编写代码。
这种方法不仅能大幅减少代码量,还能提高代码的可维护性和可扩展性。

什么是模板?

模板(Template) :是泛型编程的核心机制,它允许你编写与具体数据类型无关的通用代码,通过参数化类型(将类型作为参数)来实现代码复用。

  • 简单来说,模板就像是一个 “代码模具”,编译器会根据你使用时提供的具体类型,自动生成对应的代码。

核心概念:

  1. 参数化类型:将类型(如intdouble)作为参数传递给模板。
  2. 编译时实例化:编译器在编译阶段根据使用的具体类型生成对应代码。
  3. 类型安全:保持编译时的类型检查,避免运行时错误。

模板又分为哪些?

在 C++ 中,模板作为实现泛型编程的核心机制,依据其作用对象的不同,可清晰地划分为以下两种主要类型:

在这里插入图片描述

1.1 什么是函数模板?

函数模板(Function Template):用于创建与具体数据类型无关的通用函数,能够针对不同类型的数据执行相同逻辑的操作。

  • 通过 template <typename T>(或 class T)声明模板参数 T,代表任意数据类型
  • 编译器会在调用时根据传入的实际参数类型,自动生成对应的函数实例

语法

template <typename T>  // 声明模板参数 T
返回类型 函数名(参数列表) 
{
    // 函数体
}

示例:通用交换函数

template <typename T>  //当然也可以写成:template<class T> 注意:这里不必须将写T只是大家都习惯写T而已,因为T是“模板”英文的首字母大写
void Swap(T& a, T& b) 
{
    T temp = a;
    a = b;
    b = temp;
}

// 使用时自动推导类型
int x = 10, y = 20;
Swap(x, y);  // 编译器生成 Swap<int>(x, y)

double a = 3.14, b = 2.71;
Swap(a, b);  // 编译器生成 Swap<double>(a, b)

1.2 函数模板的原理是什么?

函数模板本质上是一种代码生成机制

  • 它如同建筑师手中的蓝图:蓝图本身不是建筑物,而是指导工人建造具体房屋的依据
  • 类似地,函数模板本身不是函数,而是编译器根据用户使用方式生成特定类型函数实例的 “模具”

通过模板,程序员只需编写一份通用代码,将数据类型抽象为参数(如:T),而将原本需要手动重复编写的具体类型实现工作,交给编译器在编译时自动完成。


注意:并不是使用了模板之后之前那些冗余的代码就可以不用写了,而是,使用了模板之后在编译器编译阶段,那些冗余的代码被编译器隐式的自动的写好了。

所以:这种 “将重复劳动自动化” 的特性,正是泛型编程最核心的优势之一。

在这里插入图片描述

1.3 怎么实例化函数模板?

函数模板的实例化:是指编译器根据用户提供的模板实参(具体类型或值),从通用的函数模板定义中生成特定类型函数的过程。

实例化方式主要有两种:隐式实例化显式实例化


隐式实例化:编译器根据函数调用时的实参类型,自动推导出模板参数的具体类型,并生成对应的函数实例。

语法

函数名(实参列表);  // 编译器自动推导模板参数类型

示例

template <typename T>
T Max(T a, T b)
{
    return a > b ? a : b;
}

int main()
{
    int x = 10, y = 20;
    int result = Max(x, y);  // 隐式实例化:推导 T 为 int,等价于 Max<int>(x, y)

    double a = 3.14, b = 2.71;
    double res = Max(a, b);  // 隐式实例化:推导 T 为 double


    //Add(x, a);
    /*
       该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
       通过实参x将T推演为int,通过实参y将T推演为double类型,但模板参数列表中只有一个T,
       编译器无法确定此处到底该将T确定为int 或者 double类型而报错
       */
       //注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
       // 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
    Add(a, (int)d);

    return 0;
}

显式实例化:在函数调用时,显式指定模板参数的具体类型,即使编译器可以自动推导。

语法

函数名<模板实参列表>(实参列表);  // 手动指定模板参数类型

示例

template <typename T>
T Add(T a, T b)
{
    return a + b;
}

int main()
{
    int sum1 = Add<int>(1, 2);              // 显式指定 T 为 int
    double sum2 = Add<double>(3.14, 2.71);  // 显式指定 T 为 double

    // 即使可以自动推导,也能显式指定
    int sum3 = Add<int>(1, 2.5);  // 显式指定 T 为 int,2.5 会被隐式转换为 2
    return 0;
}

上面实例化的模板都是只有一个模板参数的模板,其实平时我们也有声明多个模板参数的使用情况,下面博主介绍一下:多模板参数的声明实例化

//多模板参数的声明
template <typename T1, typename T2>
void PrintPair(T1 a, T2 b) 
{
    cout << a << ", " << b << endl;
}

// 隐式实例化
PrintPair(1, 3.14);  // T1=int, T2=double

// 显式实例化
PrintPair<char, string>('A', "hello");

模板实例化的总结:

  • 隐式实例化:让编译器根据调用时的实参类型自动生成函数实例(最常用)。
  • 显式实例化:手动指定模板参数类型,适用于需要精确控制类型或编译器无法推导的场景。

1.4 模板参数的匹配原则是什么?

在 C++ 中,当非模板函数与同名的函数模板并存时,函数调用的匹配规则遵循以下优先级策略

1. 优先选择非模板函数(精准匹配)

当调用的实参与非模板函数的参数类型完全匹配(无需类型转换)时,编译器会直接调用该非模板函数,即使存在一个可以实例化出相同参数类型的函数模板。

#include <iostream>
using namespace std;

/*------------非模板函数:处理int类型的特化版本(可能有优化)------------*/
void Swap(int& a, int& b)
{
    cout << "调用非模板 Swap(int&, int&)" << endl;
    int temp = a;
    a = b;
    b = temp;
}

/*------------函数模板:通用版本------------*/ 
template <typename T>
void Swap(T& a, T& b)
{
    cout << "调用模板 Swap<T>(T&, T&)" << endl;
    T temp = a;
    a = b;
    b = temp;
}

int main()
{
    int x = 1, y = 2;
    Swap(x, y);  // 优先匹配非模板函数(精准匹配int类型)

    Swap<int>(x, y);  // 显式调用模板实例化后的Swap<int>,而非普通函数

    double a = 3.14, b = 2.71;
    Swap(a, b);  // 无对应普通函数,实例化模板为Swap<double>(double&, double&)

    return 0;
}

在这里插入图片描述

2. 其次选择模板实例化(更好的匹配)

当非模板函数需要隐式类型转换才能匹配实参,而模板实例化无需转换或转换更优时,编译器会选择实例化模板

#include <iostream>
using namespace std;

// 非模板函数:仅接受double
void Func(double x)
{
    cout << "非模板: " << x << endl;
}

// 函数模板:接受任意类型
template <typename T>
void Func(T x)
{
    cout << "模板: " << x << endl;
}

int main()
{
    int d = 3;
    Func(d);  // 调用模板实例Func<int>(int),无需转换

    return 0;
}

在这里插入图片描述

------------------------------

2.1 什么是类模板?

类模板(Class Template):用于创建通用类,将数据类型作为类的参数,使类可以适配不同类型的数据。

  • 模板参数可包含一个或多个类型(如:typename T1, typename T2),甚至非类型参数(如:size_t N
  • 实例化时需显式指定类型参数,生成具体类型的类对象

语法

template <typename T>  // 声明模板参数 T
class 类名 
{
    // 类成员
};

示例:通用数组类

template <typename T, size_t N>
class Array 
{
private:
    T data[N];
public:
    T& operator[](size_t i) { return data[i]; }
    size_t size() const { return N; }
};

// 使用时指定类型和参数
Array<int, 5> arr;  // 创建存储 int 的数组,大小为 5
arr[0] = 100;

总结:两种模板类型相辅相成:

  • 函数模板 聚焦于通用函数逻辑的复用。
  • 类模板 则侧重于通用数据结构的抽象。

它们共同构成了 C++ 泛型编程的基础,使得代码能够 “一次编写,多类型适配”,显著减少冗余,提升开发效率与代码可维护性。

下面我们使用我们在《数据结构初阶》中实现的栈这种的数据结构为例,使用类模板再重新简单的实现一下,带大家感受一下“类模板”使用:

#include<iostream>
using namespace std;

/*-------------------------类模板-------------------------*/
// 类模板:定义一个通用的栈数据结构,可以存储任意类型(T)的数据
// typename T :表示这是一个类型参数,在实例化时会被具体类型(如:int、double)替换
template<typename T>
class Stack
{
public:
    /*------------构造函数,默认容量为4------------*/
    Stack(size_t capacity = 4)
    {
        _array = new T[capacity];  // 动态分配一个能存储capacity个T类型元素的数组
        _capacity = capacity;      // 记录当前栈的总容量
        _size = 0;                 // 初始化栈中元素个数为0(空栈)
    }

    /*------------声明Push函数------------*/
    void Push(const T& data);

private:
    T* _array;        // 指向存储栈元素的动态数组指针
    size_t _capacity; // 栈的总容量
    size_t _size;     // 栈当前存储的元素个数
};


/*-------------------------类模板的成员函数(在类外实现的语法)-------------------------*/
// template<class T> :表示这是一个模板函数
// void Stack<T>::Push :表示这是Stack类的Push成员函数
// 注意:模板类的成员函数实现通常要放在头文件中
template<class T>
void Stack<T>::Push(const T& data)
{
    // 这里应该添加检查是否需要扩容的逻辑
    // 目前简单实现:直接将数据放入数组

    _array[_size] = data;  
    ++_size;          
}

int main()
{
    // 实例化一个存储int类型的栈:编译器会根据Stack<int>生成一个专门处理int的栈类
    Stack<int> st1;

    // 实例化一个存储double类型的栈:编译器会生成另一个专门处理double的栈类
    Stack<double> st2;

    return 0;
}

2.2 怎么实例化类模板?

类模板的实例化:是指通过指定具体的模板实参(如:intdouble等类型),从通用的类模板定义中生成特定类型的具体类(称为模板类)的过程。

实例化方式主要有以下两种:隐式实例化显式实例化


隐式实例化:在创建对象时,显式指定模板实参,编译器自动生成对应的模板类。

语法

类模板名<模板实参列表> 对象名(构造函数参数);

示例

template <typename T>
class Vector
{
private:
    T* data;
    size_t size;
public:
    Vector() : data(nullptr), size(0) {}
    // 其他成员函数...
};

int main()
{
    Vector<int> vec1;      // 实例化Vector<int>类,存储int类型
    Vector<double> vec2;   // 实例化Vector<double>类,存储double类型
    return 0;
}

显式实例化:在代码中主动要求编译器生成特定类型的模板类,通常用于分离模板定义和声明的场景。

语法

template class 类模板名<模板实参列表>;  // 在 .cpp 文件中显式实例化

示例

/*---------------------------Vector.h(头文件)---------------------------*/
template <typename T>
class Vector
{
    /* 类定义 */
};

/*--------------------------Vector.cpp(源文件)--------------------------*/
#include "Vector.h"

// 显式实例化Vector<int>和Vector<double>
template class Vector<int>;
template class Vector<double>;

类模板的实例化总结:

在 C++ 中,类模板实例化与函数模板实例化存在明显差异。

对于函数模板实例化:

  • 编译器依据函数调用时的实参类型,自动推导出模板参数的具体类型(隐式实例化)
  • 或者根据用户显式指定的模板参数类型(显式实例化)来生成特定类型的函数

对于类模板实例化:

  • 需要在类模板名字后面紧跟<>,并将待实例化的具体类型放置在<>之中。
    • 这是因为类模板本身只是一种通用的抽象定义,并非真正意义上可直接使用的类。
    • 只有经过实例化这一过程,编译器才会依据指定的类型生成对应的具体类,此时得到的结果才是可以用于创建对象、调用成员函数等操作的真正的类。
  • 例如template <typename T> class MyClass {...}; 是类模板,而 MyClass<int> 则是经过实例化后的具体类。

类模板的实例化是通过显式指定模板实参(如:Vector<int>),让编译器生成具体类型的类。

  • 隐式实例化定义:通过类模板名 <模板实参> 对象名的方式创建对象,最常用。
  • 显式实例化定义:通过template class 类模板名<模板实参>强制编译器生成特定类型的模板类。

注意类模板这里的隐式/显式和函数模板的隐式/显式的含义并不一样,如果你觉得这些名词容易搞混,完全可以不进行记忆这些名词。

只需要记住一下两点即可:

  1. 函数模板 进行实例化的时候可以在函数模板名的后面添加<>也可不添加
  2. 类模板 进行实例化的时候必须在类模板名的后面添加<>

---------------STL简介 ---------------

什么STL?

STL(Standard Template Library,标准模板库):是 C++ 标准库的核心组成部分,提供了一系列泛型(模板化)的容器、算法和迭代器,用于高效处理数据。


显著特点:

  • 泛型编程:STL 几乎所有代码都采用模板类或模板函数。这使其不局限于特定数据类型或算法,开发者能定义自己的类型,让其与 STL 组件无缝协作,极大地增强了代码的通用性和可复用性。
  • 高性能:STL 的容器和算法都经过精心优化,在保证足够抽象层次的同时,确保了运行时的高性能,多数场景下开发者无需担忧性能问题。
  • 高移植性与跨平台:可在不同操作系统和编译器环境下使用,具有良好的兼容性。

同样的,上面的这几点也是为什么我们要学习使用STL的原因。

STL的六大核心组件是什么?

容器(Container):用于存储和管理数据的结构化单元,相当于 “数据存放的地方”。

  • 本质类模板(Class Template),通过参数化类型实现通用数据结构。
  • 分类根据数据在容器中的排列特性对容器进行分类
    • 序列式容器:元素按顺序存储,位置由插入顺序决定。
      例如vector(动态数组)、list(双向链表)、deque(双端队列)、stack(栈,适配器实现)、queue(队列,适配器实现)
    • 关联式容器:元素按关键字排序或哈希存储,支持快速查找。
      例如set/multiset(集合 / 多重集合)、map/multimap(映射 / 多重映射)、unordered_set/unordered_map(无序集合 / 映射,基于哈希表)
  • 特点:封装了底层数据结构细节,提供统一的接口(如:push_backinserterase

算法(Algorithm):用于操作容器中数据的通用函数,如:排序、查找、遍历等。

  • 本质函数模板(Function Template),通过迭代器与容器解耦。
  • 分类
    • 非修改型算法:不改变容器元素(如:findcountfor_each
    • 修改型算法:修改元素值或位置(如:sortreversecopy
    • 关联式算法:针对有序容器的操作(如:binary_searchmerge
  • 特点:不依赖具体容器类型,仅通过迭代器访问元素,实现 “一次编写,多处复用”。

迭代器(Iterator):连接容器与算法的 “桥梁”,用于遍历容器中的元素,类似 “智能指针”。

  • 本质类模板,封装了指针操作,提供统一的访问接口,如:*取值、++移动。
  • 分类:(按功能强弱排序)
    • 输入迭代器:只读,单遍扫描(如:用于istream_iterator
    • 输出迭代器:只写,单遍扫描(如:用于ostream_iterator
    • 前向迭代器:可读可写,单遍正向移动(如:list的迭代器)
    • 双向迭代器:支持正向和反向移动(如:set的迭代器)
    • 随机访问迭代器:支持任意位置跳跃(如:vector的迭代器,类似指针运算)
  • 特点:使算法不依赖容器的具体实现,只需通过迭代器接口操作元素。

仿函数(Functor):行为类似函数的类,可作为算法的参数,定制特定操作逻辑(如:排序规则、条件判断)

  • 本质重载了operator()结构体,是一种可调用对象

  • 典型应用:在sort中自定义比较规则:

    struct Greater 
    {
        bool operator()(int a, int b) { return a > b; }
    };
    
    vector<int> v = {3, 1, 2};
    sort(v.begin(), v.end(), Greater());  // 降序排序
    
  • 特点:比普通函数更灵活,可存储状态(如:成员变量),便于复用和组合。


适配器(Adapter):修改现有组件的接口,使其符合特定需求,类似 “接口转换器”。

  • 分类

    • 容器适配器:将序列式容器转换为特定接口(如:stack、queue默认基于deque实现)

      stack<int> s;  // 底层使用 deque 作为存储结构
      
    • 迭代器适配器:修改迭代器的行为(如:reverse_iterator反转遍历方向,back_inserter用于向容器尾部插入元素)

    • 仿函数适配器:修改仿函数的参数或返回值(如:negate取反、bind绑定参数)

  • 特点:不创建新组件,而是复用现有组件,通过包装实现接口转换。


空间配置器(Allocator):负责容器的内存分配、释放和管理,是 STL 的底层内存管理机制。

  • 本质类模板,封装了operator newoperator delete的底层实现。
  • 核心功能
    • 分配内存allocate()函数申请原始内存。
    • 释放内存deallocate()函数释放内存。
    • 构造 / 析构对象construct()destroy()函数处理对象生命周期。
  • 特点:允许自定义内存管理策略(如:内存池、缓存机制),提升性能或适配特殊场景。

表格总结:STL的六大核心组件

组件 作用 经典示例
容器 存储和管理数据的 通用数据结构 vector, list, map, set
算法 对容器中的数据进行操作的 函数模板 sort(), find(), reverse()
迭代器 提供访问容器元素的 统一接口 begin(), end()
仿函数 行为类似函数的 对象 greater<int>, less<string>
适配器 修饰容器或仿函数的 工具 stack, queue, priority_queue
空间配置器 控制内存分配的 策略 allocator<T>

STL的六大核心组件的大总结:

  • 容器 提供数据存储,算法 通过 迭代器 操作数据,仿函数 为算法提供自定义逻辑,适配器 调整接口,空间配置器 管理内存。
  • 这种分层设计实现了 “数据结构” 与 “算法” 的解耦,通过模板技术最大化代码复用,是泛型编程的经典实践。

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到