目录
- 引言
- 一.模板初阶一一迈向泛型编程
- 1.什么是泛型编程?
- 2.函数模板
- 1)定义与格式:
- 2)隐式实例化
- 3)显式实例化
- 4)模板参数匹配原则
- 3.类模板
- 1)类模板定义及格式
- 2)类模板的实例化
- 3)注意事项
- 二.STL初识--认识标准模板库
- 1.什么是STL
- 2.STL的版本
- 3.STL的六大组件
- 1)容器(Containers)
- 2)算法(Algorithms)
- 3)迭代器(Iterators)
- 4)仿函数 / 函数对象(Functors)
- 5)配接器 / 适配器(Adapters)
- 6)空间配置器(Allocators)
- 4.STL的重要意义与学习方法
📌相关专栏与源码导航:
- 💻配套源码:🚀 本专栏所有配套代码已同步至 GitHub,点击直达
- 📚系统专栏:
- 📖 《从零开始学习数据结构》专栏直达
- ⚡ 《C++ 深度解构》专栏直达
引言
雕版印刷:如果你要印《论语》,你就得完整地雕刻一整块《论语》的木板。如果你要印《道德经》,哪怕里面有同样的字,你通过雕版也没法复用,必须重新雕刻一整块《道德经》的木板。这叫一书一版。
活字印刷:他不是雕刻整块板,而是雕刻一个个单独的、标准化的“字模”(比如一个“之”字的字模)。这个字模本身不是一篇文章,但它是组成所有文章的通用原子。
一.模板初阶一一迈向泛型编程
1.什么是泛型编程?
在编程过程中,我们时常会遇到同一个方法但是需要重载多个不同类型的函数来使用,例如:
voidswap(int&i,int&j);voidswap(char&i,char&j);voidswap(double&i,double&j):.....痛点: \colorbox{#FFEAEA}{\color{#C22828}\text{痛点:}}痛点:
每个交换函数的逻辑是一样的,但是面对不同类型的值,Swap函数需要多次重载,逻辑重复
泛型编程:对于逻辑相同并且与类型无关的函数,定义的通用代码
这里要讲的模板便是一种不在乎变量类型的通用代码,就如活字印刷一样,哪里需要,直接放在需要的环境直接使用,需再进行相同逻辑代码的重载。
2.函数模板
还是拿交换函数作为例子来说,此函数符合模板的定义,代码逻辑一致,与参数类型无关。
1)定义与格式:
template<classT>voidSwap(T&left,T&right){//交换逻辑......}使用template关键字进行模板的定义,“< >”中括住的是模板函数的参数列表,用class或者typename来定义参数
注意: \colorbox{#FFEAEA}{\color{#C22828}\text{注意:}}注意:不可以使用struct来定义模板的参数
template<structT>//❌❌❌C++规定只能使用class或者typename来定义核心避坑 : \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑 :}}核心避坑:
模板函数本质上并不是真正的函数,编译器在编译期根据调用实参完成“类型替换与代码生成”相应参数类型的函数,并且模板函数不可以定义在主函数(main)中,禁止函数定义的嵌套。
2)隐式实例化
template<classT>voidSwap(T&left,T&right){T temp=left;left=right;right=temp;}main函数中调用模板函数
int a = 10;
int b = 20;
Swap(a, b);
我们传入实参调用模板生成相应的函数,那么这个函数是怎么知道形参的类型的呢?
编译器能够反推我们传入的实参类型,从而得到类型,然后实例化相应的函数,这便是隐式实例化(即不提供参数类型,让编译器自动推理)
⚠️ 注意: \colorbox{#FFEAEA}{\color{#C22828}\text{⚠️ 注意:}}⚠️注意:
编译器并不会一定能反推得到参数类型
- 模板参数作为返回值
template<classT>Tfunc(){returnT;//❗❗❗只作为返回值返回}我们从始至终都没有传入实参,所以编译器无法通过反推来得到参数类型,无法得知需要返回的值是什么类型。
- 传入参数类型不匹配
template<classT>TAdd(T t1,T t2){returnt1+t2;}inta=10;doubleb=1.1;Add(a,b);///❗❗❗❗传入的实参类型不同这里编译器会疑惑,通过a推导出T类型为整型,但是通过b又推导出T类型为浮点型,为了保证类型安全,编译器绝对不会擅作主张,将谁隐式转换为谁,所以会报错
3)显式实例化
既然隐式推导有局限性,那么程序员在调用时明确参数类型的使用方式,便被称为显式实例化。
使用方法:函数名<参数类型>(实参)
解决方案:
template<classT>Tfunc(){returnT;}intx=func<int>();通过标明参数类型,这样编译器便可以理解我们的意思了
对于参数不同的传递,编译器不会自动隐式转换,所以也可以用显式实例化来调用
template<classT>TAdd(T t1,T t2){returnt1+t2;}inta=10;doubleb=1.1;intret=Add<int>(a,b);//double ret = Add<double>(a, b)4)模板参数匹配原则
当一个普通函数(非模板函数)和一个同名的模板函数同时存在,编译器会如何抉择呢?
intAdd(inta,intb){returna+b;}template<classT>TAdd(T t1,T t2){returnt1+t2;}inta=1;intb=2;intret1=Add(a,b);intret2=Add<>(a,b);intret3=Add(a,1.2);当函数模板和普通函数名相同时,优先使用普通函数(有现成用现成),如果想要强制使用模板函数需要在函数名后加上<>,类似于显式实例化,对于ret3来说,普通函数会通过隐式类型转换进行匹配,但是如果支持两参模板函数则优先实例化更匹配的。
匹配原则总结: \colorbox{#FFEAEA}{\color{#C22828}\text{匹配原则总结:}}匹配原则总结:
1.有现成用现成
2.有最匹配的用最匹配的
3.类模板
不仅仅函数可以复用,对于类类型也可以被设计成模板。
1)类模板定义及格式
类模板作用:
在 C 语言学习中,我们要实现一个通用的栈,往往使用 typedef int DataType;。但这带来一个致命缺陷:在同一个程序中,无法同时拥有一个存 int 的栈和一个存 double 的栈。这时我们只需要写一个类模板,就能同时拥有int和double栈了
template<classT>classStack{public:Stack(size_t capacity=4):_str[newT[capacity]],_capacity(capacity),_size(0){}~Stack(){delete[]_str;_str=nullptr;}//成员函数声明voidPush(constT&data);private:T*_str;size_t _size;size_t _capacity;};template<classT>voidStack<T>::Push(constT&data){....}思考: \colorbox{#FFEAEA}{\color{#C22828}\text{思考:}}思考:
这里的Stack是为了显式提供参数类型吗,并不是,这里之所以写是为了告诉编译器,这是类模板的成员函数,显式指定参数的写法是s.push(10)
| ⚠️ 注意:模板可以具有非类型参数,用来指定大小 |
template<classT,size_t Size>classStack{private:T _str[Size];// ✅✅✅直接在对象内部/栈上开辟空间size_t _size{0};public:Stack()=default;size_tcapacity()const{returnSize;}};这样可以直接在栈上开辟空间,而不需要new来在堆上申请空间
2)类模板的实例化
类模板的实例化是否需要显式提供模板参数类型,取决于构造函数
//构造函数Stack(size_t capacity=4):_str[newT[capacity]],_capacity(capacity),_size(0){}例如在这里我们的构造函数如果不提供模板参数类型,那么就无法得知需要new什么类型的空间,
但是如果构造函数和类型无关则可以不显示传入模板参数类型
template<classT>classStack(){public:Stack(T x){}};Stackst1(10);这里的构造函数与类型无关,所以可以不显示传入
注意:C++17后的这种情况才可以不指定模板参数 \colorbox{#FFEAEA}{\color{#C22828}\text{注意:C++17后的这种情况才可以不指定模板参数}}注意:C++17后的这种情况才可以不指定模板参数
类模板和模板类关系: \colorbox{#FFEAEA}{\color{#C22828}\text{类模板和模板类关系:}}类模板和模板类关系:
1.类模板是类的一个模板,是实例化类的蓝图,本身不是一个具体的类
2.类模板是通过模板类实例化出来的真正的类,类型已经被确定
3)注意事项
1.类模板并不是真正具体的类,只是一个模板,需要我们指定类型后,才会实例化出具体的类
2.类模板的模板参数最多可以有多个
template<classT1,classT2>classData{};注意: \colorbox{#FFEAEA}{\color{#C22828}\text{注意:}}注意:
模板的虚拟参数不但可以使用class 还可以使用typename,也可以将class和typename混用,例如
template<classT1,typenameT2>classData{};3.模板类函数不能像普通类一样声明定义分离在两个文件中
🔹Stack.h
声明类和成员函数
🔹Stack.cpp
定义成员函数
🔹main.cpp
创建对象并调用函数
二.STL初识–认识标准模板库
1.什么是STL
STL(Standard Template Library,标准模板库):是 C++ 标准库的核心组成部分,是一个惠及全球 C++ 程序员的、由数据结构与算法结合的庞大软件框架。
在过去用 C 语言写代码时,如果我们想用一个链表或者一个顺序表,往往需要自己从零手写增删查改逻辑;更痛苦的是,换了一个项目或者换了一个数据类型,相同的逻辑又得再重写一遍。
通过上一部分的学习我们知道,模板就是为了解决“重复实现与类型无关代码”的利器。而惠普实验室的 Alexander Stepanov 等专家,便利用模板技术将计算机领域中最常用的数据结构与通用算法进行了高度工业级的标准化封装,最终并入 C++ 标准库,这便是 STL。
通俗理解: \colorbox{#FFEAEA}{\color{#C22828}\text{通俗理解:}}通俗理解:
模板就如同活字印刷中的“字模技术”,而 STL 就是官方提前帮我们雕刻好、排版好的一整套工业级“字模库”。开箱即用,无需再造轮子。
2.STL的版本
在学习 STL 源码与底层实现时,了解版本能帮我们避免很多头文件与底层命名上的困惑:
- HP 版本:Alexander Stepanov 等人在惠普实验室开发的原始版本,是所有后续商业与开源版本的鼻祖。
- P.J. Plauger 版本:由 P.J. Plauger 博士编写并卖给微软,也就是我们常用的Visual Studio(MSVC)所内置的版本。特点是命名风格严谨(下划线和大小写较多),但代码阅读难度相对较高。
- SGI 版本:Silicon Graphics Computer Systems 公司开发的版本,被 GCC(Linux 平台)深度吸收。其代码排版优美、可读性极高,是**侯捷老师《STL源码剖析》**所讲解的版本,也是我们后续深入学习底层原理与模拟实现的首选参考版本。
3.STL的六大组件
STL 并不是一个单体结构,而是由以下六大组件相互支撑构成的有机整体:
+-------------------------------------------------------+ | Container (容器) | | (vector, list, deque, map, set ...) | +-------------------------------------------------------+ | | v v +-----------------------+ +-----------------------+ | Iterator (迭代器) |<=====>| Algorithm (算法) | | (连接容器与算法的桥梁) | | (sort, find, copy...) | +-----------------------+ +-----------------------+ ^ ^ | | +-----------------------+ +-----------------------+ | Adapter (配接器) | | Functor (仿函数) | | (stack, queue ...) | | (less, greater ...) | +-----------------------+ +-----------------------+ | | +---------------+---------------+ | v +-----------------------+ | Allocator (空间配置器)| | (负责底层高效内存管理)| +-----------------------+1)容器(Containers)
专门用来组织与管理数据的模板类(如动态数组vector、双向链表list、红黑树结构map/set等)。
2)算法(Algorithms)
解决问题的通用逻辑步骤(如sort、find、reverse、binary_search等),全部以函数模板的形式存在。
3)迭代器(Iterators)
扮演容器与算法之间的粘合剂。它是一种行为类似于指针的模板类对象,算法通过迭代器来访问容器中的元素,而无需知晓容器底层到底是用数组、链表还是二叉树实现的。
4)仿函数 / 函数对象(Functors)
重载了函数调用操作符operator()的类对象,其行为像函数一样可以被传入算法中,用来定制比较策略或特定操作(如greater<T>控制降序)。
5)配接器 / 适配器(Adapters)
一种用于“接口转换”的组件。比如后文会学到的stack(栈)和queue(队列),它们在 STL 中并不是独立的数据结构,而是将deque或list的接口进行了二次封装后形成的配接器。
6)空间配置器(Allocators)
负责内存的动态开辟与释放。所有容器默认都带有空间配置器,其内部通过二级内存池机制极大缓解了频繁申请小块内存导致的内存碎片和效率开销问题。
思考: \colorbox{#FFEAEA}{\color{#C22828}\text{思考:}}思考:
为什么遍历数组用int*,遍历链表却要用Node*?
在 STL 中,无论底层结构差异多大,迭代器都统一重载了*(解引用获取值)、++(移向下一个元素)、!=(比较边界)。正是因为迭代器抹平了不同数据结构之间的访问差异,算法才能够做到与容器完全解耦!
4.STL的重要意义与学习方法
核心避坑 : \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑 :}}核心避坑:
千万不要把 STL 仅仅当成一堆死记硬背的 API 来学!
在日常学习与未来的求职面试中,STL 的掌握通常分为三重境界:
- 熟练使用(第一层):能熟练运用
vector、list、map等完成业务逻辑与力扣刷题。 - 理解底层与避免陷阱(第二层):搞懂扩容机制(1.5倍还是2倍?)、各操作的时间复杂度、内存对齐与最致命的迭代器失效问题。
- 动手模拟实现(第三层):按照 SGI-STL 的设计思想,亲手手写模拟实现一个简化版的
string、vector、list以及它们的迭代器类,彻底打通深拷贝、现代写法、泛型封装的底层认知。
在接下来的篇章中,我们将从最亲民的字符容器序列——string类的原理剖析与模拟实现正式起步!
“不必急于证明自己,沉下心走好脚下的每一步,时间自会给出答案。”
Crafted with passion byHhy
当前更新时间:{{ 2026 / 10 / 21 }} | 此博文系博主学习复盘,欢迎指正
本文专栏由 Hhy 维护。如果这篇文章曾带给你启发,请考虑关注我的 CSDN 博客或 GitHub 仓库以获取最新技术分享。