《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)
:是泛型编程的核心机制,它允许你编写与具体数据类型无关的通用代码,通过参数化类型(将类型作为参数)来实现代码复用。
- 简单来说,模板就像是一个 “代码模具”,编译器会根据你使用时提供的具体类型,自动生成对应的代码。
核心概念:
- 参数化类型:将类型(如
int
、double
)作为参数传递给模板。- 编译时实例化:编译器在编译阶段根据使用的具体类型生成对应代码。
- 类型安全:保持编译时的类型检查,避免运行时错误。
模板又分为哪些?
在 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 怎么实例化类模板?
类模板的实例化
:是指通过指定具体的模板实参(如:int
、double
等类型),从通用的类模板定义中生成特定类型的具体类(称为模板类)的过程。实例化方式主要有以下两种:
隐式实例化
和显式实例化
隐式实例化
:在创建对象时,显式指定模板实参,编译器自动生成对应的模板类。语法:
类模板名<模板实参列表> 对象名(构造函数参数);
示例:
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 类模板名<模板实参>
强制编译器生成特定类型的模板类。注意:
类模板
这里的隐式/显式和函数模板
的隐式/显式的含义并不一样,如果你觉得这些名词容易搞混,完全可以不进行记忆这些名词。只需要记住一下两点即可:
- 函数模板 进行实例化的时候
可以
在函数模板名的后面添加<>
也可不添加- 类模板 进行实例化的时候
必须
在类模板名的后面添加<>
---------------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_back
、insert
、erase
)
算法(Algorithm)
:用于操作容器中数据的通用函数,如:排序、查找、遍历等。
- 本质:函数模板(Function Template),通过迭代器与容器解耦。
- 分类:
非修改型算法
:不改变容器元素(如:find
、count
、for_each
)修改型算法
:修改元素值或位置(如:sort
、reverse
、copy
)关联式算法
:针对有序容器的操作(如:binary_search
、merge
)- 特点:不依赖具体容器类型,仅通过迭代器访问元素,实现 “一次编写,多处复用”。
迭代器(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 new
和operator 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的六大核心组件的大总结:
容器
提供数据存储,算法
通过迭代器
操作数据,仿函数
为算法提供自定义逻辑,适配器
调整接口,空间配置器
管理内存。- 这种分层设计实现了 “数据结构” 与 “算法” 的解耦,通过模板技术最大化代码复用,是泛型编程的经典实践。