> 技术文档 > 《C++初阶之STL》【泛型编程 + STL简介】

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

语法

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

示例:通用交换函数

template <typename T> //当然也可以写成:template 注意:这里不必须将写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(x, y)double a = 3.14, b = 2.71;Swap(a, b); // 编译器生成 Swap(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(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 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&)\" << endl; T temp = a; a = b; b = temp;}int main(){ int x = 1, y = 2; Swap(x, y); // 优先匹配非模板函数(精准匹配int类型) Swap<int>(x, y); // 显式调用模板实例化后的Swap,而非普通函数 double a = 3.14, b = 2.71; Swap(a, b); // 无对应普通函数,实例化模板为Swap(double&, double&) return 0;}

在这里插入图片描述

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

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

#include using namespace std;// 非模板函数:仅接受doublevoid 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),无需转换 return 0;}

在这里插入图片描述

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

2.1 什么是类模板?

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

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

语法

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

示例:通用数组类

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 的数组,大小为 5arr[0] = 100;

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

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

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

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

#includeusing 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 :表示这是一个模板函数// void Stack::Push :表示这是Stack类的Push成员函数// 注意:模板类的成员函数实现通常要放在头文件中template<class T>void Stack<T>::Push(const T& data){ // 这里应该添加检查是否需要扩容的逻辑 // 目前简单实现:直接将数据放入数组 _array[_size] = data; ++_size; }int main(){ // 实例化一个存储int类型的栈:编译器会根据Stack生成一个专门处理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类型 Vector<double> vec2; // 实例化Vector类,存储double类型 return 0;}

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

语法

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

示例

/*---------------------------Vector.h(头文件)---------------------------*/template <typename T>class Vector{ /* 类定义 */};/*--------------------------Vector.cpp(源文件)--------------------------*/#include \"Vector.h\"// 显式实例化Vector和Vectortemplate class Vector<int>;template class Vector<double>;

类模板的实例化总结:

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

对于函数模板实例化:

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

对于类模板实例化:

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

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

  • 隐式实例化定义:通过类模板名 对象名的方式创建对象,最常用。
  • 显式实例化定义:通过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, less 适配器 修饰容器或仿函数的 工具 stack, queue, priority_queue 空间配置器 控制内存分配的 策略 allocator

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

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

在这里插入图片描述