深入理解C++系列(06)——模版初阶与STL

文章目录

  • [一、C++ 模板初阶](#一、C++ 模板初阶)
    • 一、本节核心主线
    • 二、泛型编程
      • [1. 为什么需要泛型编程?](#1. 为什么需要泛型编程?)
      • [2. 泛型编程的思想](#2. 泛型编程的思想)
    • 三、函数模板
      • [1. 函数模板概念](#1. 函数模板概念)
      • [2. 函数模板格式](#2. 函数模板格式)
      • [3. 通用 Swap 函数模板](#3. 通用 Swap 函数模板)
    • 四、函数模板的原理⭐️⭐️⭐️
      • [1. 函数模板不是函数](#1. 函数模板不是函数)
      • [2. 模板把重复工作交给编译器](#2. 模板把重复工作交给编译器)
    • 五、函数模板实例化⭐️
    • [1. 隐式实例化(常用)](#1. 隐式实例化(常用))
    • [2. 隐式实例化的类型冲突](#2. 隐式实例化的类型冲突)
    • [3. 解决方式一:强制类型转换](#3. 解决方式一:强制类型转换)
    • [4. 解决方式二:显式实例化](#4. 解决方式二:显式实例化)
    • 六、模板参数匹配原则
      • [1. 普通函数和函数模板可以同名共存](#1. 普通函数和函数模板可以同名共存)
      • [2. 普通函数和模板都匹配时,优先普通函数](#2. 普通函数和模板都匹配时,优先普通函数)
      • [3. 如果模板能生成更匹配的版本,会选择模板](#3. 如果模板能生成更匹配的版本,会选择模板)
      • [4. 模板一般不主动做类型转换](#4. 模板一般不主动做类型转换)
    • 七、类模板
    • [1. 类模板定义格式](#1. 类模板定义格式)
    • [2. 类模板成员函数在类外定义](#2. 类模板成员函数在类外定义)
    • [3. 模板不建议声明和定义分离到 `.h` 和 `.cpp`⭐️⭐️⭐️⭐️](#3. 模板不建议声明和定义分离到 .h.cpp⭐️⭐️⭐️⭐️)
    • 八、类模板实例化
    • 九、易错点汇总
      • [易错点 1:把函数模板当成函数](#易错点 1:把函数模板当成函数)
      • [易错点 2:模板参数中用 `struct`](#易错点 2:模板参数中用 struct)
      • [易错点 3:同一个 `T` 被推导成不同类型](#易错点 3:同一个 T 被推导成不同类型)
      • [易错点 4:以为模板会自动类型转换](#易错点 4:以为模板会自动类型转换)
      • [易错点 5:类模板实例化忘记写 `<类型>`](#易错点 5:类模板实例化忘记写 <类型>)
      • [易错点 6:类模板成员函数类外定义忘记 `Stack<T>::`](#易错点 6:类模板成员函数类外定义忘记 Stack<T>::)
    • 十、本节总结
  • [二、STL 简介复习总结](#二、STL 简介复习总结)
    • 一、本讲核心主线
    • [二、什么是 STL](#二、什么是 STL)
    • [三、STL 的版本](#三、STL 的版本)
      • [1. 原始版本 / HP 版本](#1. 原始版本 / HP 版本)
      • [2. P. J. 版本](#2. P. J. 版本)
      • [3. RW 版本](#3. RW 版本)
      • [4. SGI 版本](#4. SGI 版本)
    • [四、STL 的六大组件](#四、STL 的六大组件)
      • [1. 容器](#1. 容器)
      • [2. 算法](#2. 算法)
      • [3. 迭代器](#3. 迭代器)
      • [4. 仿函数](#4. 仿函数)
      • [5. 适配器](#5. 适配器)
      • [6. 空间配置器](#6. 空间配置器)
    • 五、本节总结⭐️⭐️
    • 下期预告
    • string
    • 结语

◆ 博主: @此生决int
分享编程知识!深挖底层原理!持续原创更新!

热门专栏:

深入理解 C++系列|算法系列

快速复习系列 |Java 速通系列

📌 本系列知识点前后关联较强,建议按照专栏顺序阅读哦

本系列主要面向的阅读人群

1,学完 C 语言,准备系统学习 C++ 的同学

2,已经学习过 C++,希望重新梳理知识体系的同学

3,想深入理解 C++ 设计思想与底层原理,而不仅仅停留在语法层面的同学

不止教你会写代码,更带你理解代码为什么这样设计!

上期回顾

  hello,大家好,这里是 此生决int,上一期呢我们学习了C++中数据的存储,新学习了new delete等等C++申请和释放内存的方式 ,那么今天我们再来学习一个C++面向对象的一个厉害的地方,模版和STL ,这可以算的上是C++区别于C语言的最大的一个特点,甚至有人说,C语言+STL就是C++!!!那么今天我们就来学习学习这个模版和STL!

一、C++ 模板初阶


一、本节核心主线

模板初阶主要解决一个问题:

如何写一份与类型无关的通用代码,让编译器根据不同类型自动生成对应代码?


二、泛型编程

1. 为什么需要泛型编程?

如果要写交换函数,普通做法可能是函数重载:

cpp 复制代码
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;
}

问题:太麻烦了!

  1. 代码逻辑完全一样,只是类型不同。
  2. 每出现一个新类型,就要新增一个函数。
  3. 代码复用率低。
  4. 维护成本高,一个逻辑错误可能每个重载版本都要改。

2. 泛型编程的思想

泛型编程:深刻理解与类型无关的通用代码!!!

编写与类型无关的通用代码,是代码复用的一种重要手段。

模板就是泛型编程的基础。可以把模板理解成一个"模具":

text 复制代码
模板 = 模具
类型 = 材料
实例化后的函数/类 = 具体成品

三、函数模板

1. 函数模板概念

函数模板代表一个函数家族。就是众多只是类型不同的函数里面的类型参数被抽象成了一个T!


2. 函数模板格式

cpp 复制代码
template<class T>//class或者typename都可以,但是不能用 `struct`
template<typename T>
返回值类型 函数名(参数列表)
{
}

3. 通用 Swap 函数模板

cpp 复制代码
template<typename T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}

调用:

cpp 复制代码
int a = 1, b = 2;
Swap(a, b);

double x = 1.1, y = 2.2;
Swap(x, y);

编译器会根据实参类型生成:

cpp 复制代码
void Swap(int& left, int& right);
void Swap(double& left, double& right);

注意哦,编译器会生成对应的函数,根据模版,但是是你要什么生成什么而不是全部生成!


四、函数模板的原理⭐️⭐️⭐️

1. 函数模板不是函数

函数模板本身不是函数 ,而是编译器生成具体函数的蓝图。编译期间,编译器会根据模板使用情况生成具体类型的函数。

例如:

cpp 复制代码
Swap(a, b); // a、b 是 int

编译器推导出:

cpp 复制代码
T = int

于是生成一份处理 intSwap 函数。


2. 模板把重复工作交给编译器

原来需要程序员手写:

cpp 复制代码
Swap(int&)
Swap(double&)
Swap(char&)

使用模板后:

程序员写一份模板,编译器按需生成多份具体函数。


五、函数模板实例化⭐️

用不同类型的参数使用函数模板,称为函数模板实例化。

分为:

  1. 隐式实例化
  2. 显式实例化

1. 隐式实例化(常用)

让编译器根据实参类型推导模板参数。

cpp 复制代码
template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}

int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;

Add(a1, a2); // T 推导为 int
Add(d1, d2); // T 推导为 double

2. 隐式实例化的类型冲突

cpp 复制代码
Add(a1, d1);

如果模板是:

cpp 复制代码
template<class T>
T Add(const T& left, const T& right)

编译器推导:

  • 根据 a1 推导 T = int
  • 根据 d1 推导 T = double

同一个 T 推导出两个类型,编译器无法决定,因此报错。


3. 解决方式一:强制类型转换

cpp 复制代码
Add(a1, (int)d1);

此时两个实参都可以推导为 int


4. 解决方式二:显式实例化

显式指定模板参数类型

cpp 复制代码
Add<int>(a1, d1);

表示强制实例化出 T = int 的版本。

如果类型不匹配,编译器会尝试隐式类型转换。


六、模板参数匹配原则

1. 普通函数和函数模板可以同名共存

cpp 复制代码
int Add(int left, int right)
{
    return left + right;
}

template<class T>
T Add(T left, T right)
{
    return left + right;
}

2. 普通函数和模板都匹配时,优先普通函数

cpp 复制代码
Add(1, 2);

如果普通函数完全匹配,编译器优先调用普通函数,而不是从模板生成一个实例。


3. 如果模板能生成更匹配的版本,会选择模板

例如:

cpp 复制代码
int Add(int left, int right);

template<class T1, class T2>
T1 Add(T1 left, T2 right);

Add(1, 2.0);

普通函数需要把 2.0 转成 int

模板可以生成:

cpp 复制代码
Add(int, double)

更匹配,所以会选择模板。


4. 模板一般不主动做类型转换

重点:

在模板中,编译器一般不会进行类型转换操作,因为一旦转换出问题,编译器就需要背锅。

例如:

cpp 复制代码
template<class T>
T Add(T left, T right);

Add(1, 2.0); // T 推导冲突,通常报错

除非你显式指定模板参数:

cpp 复制代码
Add<int>(1, 2.0);

七、类模板

1. 类模板定义格式

cpp 复制代码
template<class T1, class T2, ..., class Tn>
class 类模板名
{
    // 类内成员定义
};

示例:

cpp 复制代码
template<typename T>
class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        _array = new T[capacity];
        _capacity = capacity;
        _size = 0;
    }

    void Push(const T& data);

private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

2. 类模板成员函数在类外定义

类外定义成员函数时要写模板参数:

cpp 复制代码
template<class T>
void Stack<T>::Push(const T& data)
{
    _array[_size] = data;
    ++_size;
}

注意两点:

1. 前面要写:

cpp 复制代码
template<class T>

2. 类名位置要写:

cpp 复制代码
Stack<T>::

3. 模板不建议声明和定义分离到 .h.cpp⭐️⭐️⭐️⭐️

在此强调:

模板不建议 像普通函数那样声明放 .h、定义放 .cpp,否则容易出现链接错误

当前阶段先记结论:

模板通常把声明和定义都放在头文件中

后面深入模板编译原理时再理解原因。


八、类模板实例化

类模板实例化和函数模板不同。

函数模板可以根据实参自动推导:

cpp 复制代码
Swap(a, b);

类模板使用时通常需要显式写类型:

cpp 复制代码
Stack<int> st1;
Stack<double> st2;

重点:

Stack 是类模板名,不是真正的类型;Stack<int> 才是真正的类型


九、易错点汇总

易错点 1:把函数模板当成函数

函数模板不是函数,而是生成函数的模具。


易错点 2:模板参数中用 struct

正确:

cpp 复制代码
template<typename T>
template<class T>

错误:

cpp 复制代码
template<struct T>

易错点 3:同一个 T 被推导成不同类型

cpp 复制代码
Add(1, 2.0);

如果模板只有一个 T,编译器无法同时让 Tintdouble


易错点 4:以为模板会自动类型转换

模板推导阶段通常不会主动做类型转换。

要么手动强转:

cpp 复制代码
Add(1, (int)2.0);

要么显式实例化:

cpp 复制代码
Add<int>(1, 2.0);

易错点 5:类模板实例化忘记写 <类型>

错误:

cpp 复制代码
Stack st;

正确:

cpp 复制代码
Stack<int> st;

易错点 6:类模板成员函数类外定义忘记 Stack<T>::

正确:

cpp 复制代码
template<class T>
void Stack<T>::Push(const T& data)
{
}

十、本节总结

  • 泛型编程:写与类型无关的通用代码。
  • 模板是泛型编程的基础。
  • 函数模板不是函数,是生成函数的蓝图。
  • 模板实例化:编译器根据类型生成具体函数或类。
  • 隐式实例化:编译器根据实参推导类型。
  • 显式实例化:程序员用 <> 指定类型。
  • 同一个模板参数不能同时推导成多个类型。
  • 普通函数和模板都匹配时,普通函数优先。
  • 模板能生成更匹配版本时,会选择模板。
  • 模板一般不会在推导阶段主动做类型转换。
  • Stack 是类模板名,Stack<int> 才是真正类型。
  • 模板定义通常放头文件,不建议声明定义分离到 .h/.cpp

二、STL 简介复习总结

本节内容偏了解,可直接跳转到总结部分!


一、本讲核心主线

这一讲主要是 STL 的入门介绍,重点不是具体使用细节,而是建立整体框架:

  1. STL 是什么
  2. STL 有哪些版本
  3. STL 的六大组件
  4. STL 为什么重要
  5. STL 应该怎么学

一句话概括:

STL 是 C++ 标准库的重要组成部分,它把常用数据结构和算法做成了可复用的软件框架,让我们不用重复造轮子。


二、什么是 STL

STL 全称:

text 复制代码
Standard Template Library
标准模板库

它是 C++ 标准库的重要组成部分。

STL 不只是一个简单的组件库,而是:

包含数据结构与算法的软件框架。

它提供了大量通用组件,例如:

  • 顺序表 / 动态数组:vector
  • 链表:list
  • 栈:stack
  • 队列:queue
  • 优先级队列:priority_queue
  • 集合:set
  • 映射:map
  • 哈希表相关容器:unordered_mapunordered_set
  • 通用算法:排序、查找、遍历等

这些内容也就是我们接下来要学习的内容!!


三、STL 的版本

本节我们介绍几个 STL 版本,复习时不需要死背细节,但要知道 SGI 版本比较重要。


1. 原始版本 / HP 版本

由:

  • Alexander Stepanov
  • Meng Lee

在惠普实验室完成。

特点:

  • STL 的原始版本。
  • 具有开源精神。
  • 是所有 STL 实现版本的始祖。

2. P. J. 版本

由 P. J. Plauger 开发。

特点:

  • 继承自 HP 版本。
  • 被 Windows Visual C++ 采用。
  • 不能公开或修改。
  • 可读性较低,符号命名较怪。

3. RW 版本

由 Rouge Wage 公司开发。

特点:

  • 继承自 HP 版本。
  • 被 C++ Builder 采用。
  • 不能公开或修改。
  • 可读性一般。

4. SGI 版本

由 Silicon Graphics Computer Systems 公司开发。

特点:

  • 继承自 HP 版本。
  • 被 GCC / Linux 采用。
  • 可移植性好。
  • 可以公开、修改甚至贩卖。
  • 命名风格和编程风格可读性较高。

重点:

后续学习 STL 源码时,主要参考 SGI 版本。


四、STL 的六大组件

STL 通常分为六大组件:目前只做了解,后面都会详细讲

组件 作用
容器 存储数据的数据结构
算法 操作数据的通用算法
迭代器 连接容器和算法,类似泛化指针
仿函数 像函数一样使用的对象
适配器 对已有组件进行包装转换
空间配置器 负责内存申请和释放

1. 容器

容器用于存储数据。

常见容器:

cpp 复制代码
vector
list
deque
stack
queue
priority_queue
set
map
unordered_set
unordered_map

可以把容器理解为:

STL 中封装好的常用数据结构。


2. 算法

算法用于操作容器中的数据。例如:

cpp 复制代码
sort
find
count
reverse
lower_bound
upper_bound

算法通常不直接依赖某一种容器,而是通过迭代器操作数据。


3. 迭代器

迭代器是 STL 中非常核心的概念。

作用:

连接容器和算法。

可以把迭代器先粗略理解为:

类似指针的东西,用来访问容器中的元素

例如:

cpp 复制代码
vector<int> v;
vector<int>::iterator it = v.begin();

后面学习 STL 时,迭代器是重点。


4. 仿函数

仿函数是像函数一样使用的对象。

本质通常是重载了 operator() 的类对象。

例如后续可能看到:

cpp 复制代码
greater<int>()
less<int>()

当前阶段了解即可。


5. 适配器

适配器是对已有组件做包装,让它表现出另一种使用形式。

典型例子:

cpp 复制代码
stack
queue
priority_queue

它们底层通常依赖已有容器实现,但对外提供特定接口。

例如:

  • stack 提供栈接口。
  • queue 提供队列接口。
  • priority_queue 提供堆 / 优先级队列接口。

6. 空间配置器

空间配置器负责内存管理。

它是 STL 底层组件,主要负责:

  • 空间申请
  • 空间释放
  • 对象构造
  • 对象析构

当前阶段知道它是 STL 的底层内存管理组件即可。


五、本节总结⭐️⭐️

  • STL = Standard Template Library,标准模板库。
  • STL 是 C++ 标准库的重要组成部分。
  • STL 是数据结构和算法的软件框架。
  • STL 六大组件:容器、算法、迭代器、仿函数、适配器、空间配置器。
  • 容器负责存数据。
  • 算法负责处理数据。
  • 迭代器负责连接容器和算法。
  • 适配器是对已有组件的包装转换。
  • 学习 STL 的三层境界:能用、明理、能扩展。
  • 后续源码学习主要参考 SGI 版本。
  • STL 不只是容器,还包括算法、迭代器、仿函数、适配器、空间配置器。
  • stackqueuepriority_queue 更准确地说是容器适配器。
  • 学 STL 不能只停留在会调接口,面试还要理解底层原理。
  • 迭代器是连接容器和算法的核心,后续要重点掌握。
  • SGI STL 版本可读性较高,后续源码学习主要参考它。

下期预告

string

结语

**  本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**

**  也欢迎订阅我的**

深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++

算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线

快速复习系列:知识梳理、查漏补缺,考前冲刺必备

Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试


**  愿每一次敲下键盘,都比昨天更进一步!** **  愿每一行代码落下,都让未来多一种可能!**

相关推荐
c238562 小时前
《枚举算法 “翻译官”:用 enum class 给 “游游的礼盒” 算分》
c语言·c++·算法
swany2 小时前
同步数据中,只需要几秒钟 & milvus向量数据库不可用 dify1.16.1 升级后踩坑记录
开发语言·python·numpy
小杍随笔2 小时前
2025年Rust GUI框架实战万字避坑指南
开发语言·后端·rust
noipp2 小时前
推荐题目:洛谷 P5843 [SCOI2012] Blinker 的噩梦
c语言·数据结构·c++·算法·游戏·洛谷·luogu
geovindu2 小时前
CSharp: LogHelper
开发语言·后端·c#·.net
奈斯先生Vector2 小时前
大模型 Agentic Workflow 架构解构:异构 API 调度与 Token 路由的多模态系统设计
开发语言·前端·架构·prompt·aigc·音视频
冻柠檬飞冰走茶2 小时前
PTA基础编程题目集 7-20 打印九九口诀表(C语言实现)
c语言·开发语言·数据结构·算法
鱼子星_2 小时前
《算 · 法》题目解析篇(1):最近公共祖先问题,线段树,最长回文子序列
c++·算法·动态规划·递归
Richard.Wong3 小时前
qt生成dll供C#调用
开发语言·qt