当前位置: 首页 > news >正文

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

【泛型编程 + STL简介】

  • 前言:
  • ---------------泛型编程 ---------------
    • 什么是泛型编程?
    • 为什么要引入泛型编程?
    • 什么是模板?
    • 模板又分为哪些?
      • 1.1 什么是函数模板?
      • 1.2 函数模板的原理是什么?
      • 1.3 怎么实例化函数模板?
      • 1.4 模板参数的匹配原则是什么?
      • ------------------------------
      • 2.1 什么是类模板?
      • 2.2 怎么实例化类模板?
  • ---------------STL简介 ---------------
    • 什么STL?
    • STL的六大核心组件是什么?

在这里插入图片描述

往期《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 为 intdouble sum2 = Add<double>(3.14, 2.71);  // 显式指定 T 为 double// 即使可以自动推导,也能显式指定int sum3 = Add<int>(1, 2.5);  // 显式指定 T 为 int,2.5 会被隐式转换为 2return 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的六大核心组件的大总结:

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

在这里插入图片描述

http://www.dtcms.com/a/278486.html

相关文章:

  • Spring原理揭秘--初识AOP
  • Spring 学习笔记
  • UI前端大数据处理新挑战:如何高效处理实时数据流?
  • JavaScript 与 C语言基础知识差别
  • GO语言中的垃圾回收(GC)
  • 怎么挑选最新贝琪入门电钢琴才高效?
  • Java进程、线程与协程对比
  • GD32/STM32嵌入CMSIS-DSP的库(基于Keil)
  • 2025年 GitHub 主流开源视频生成模型介绍
  • Go语言第一个程序--hello world!
  • arthas:Java 应用问题诊断利器
  • 企业培训笔记:axios 发送 ajax 请求
  • vue中计算属性的介绍
  • 前端基础知识TypeScript 系列 - 08(TypeScript 装饰器的理解)
  • 代理模式详解:代理、策略与模板方法模式
  • SpringMVC1
  • GraphRAG核心提示词工程完整中文版
  • VyOS起步指南:用Docker快速搭建网络实验环境
  • 分享三个python爬虫案例
  • HTML应用指南:利用GET请求获取河南省胖东来超市门店位置信息
  • STM32新建工程
  • HTB 赛季8靶场 - Outbound
  • 微算法科技技术创新,将量子图像LSQb算法与量子加密技术相结合,构建更加安全的量子信息隐藏和传输系统
  • 复习笔记 38
  • 安卓基于 FirebaseAuth 实现 google 登录
  • 【小米训练营】C++方向 实践项目 Android Player
  • C++ 左值右值、左值引用右值引用、integral_constant、integral_constant的元模板使用案例
  • 量子计算新突破!阿里“太章3.0”实现512量子比特模拟(2025中国量子算力巅峰)
  • ethers.js-5–和solidity的关系
  • RPC 框架学习笔记