C++模版:模板初阶及STL简介
引言
在 C++ 中,模板(Template) 是实现泛型编程(Generic Programming) 的核心机制,它允许开发者编写与数据类型无关的通用代码,从而实现代码复用并保证类型安全。简单来说,模板就像一个 "代码生成器",编译器会根据传入的具体类型,自动生成针对该类型的具体代码。
一. 泛型编程
1.1 什么是泛型编程
泛型编程(Generic Programming)是一种独立于具体数据类型的编程范式,其核心思想是:通过将数据类型参数化,编写可复用、通用的代码,使得同一套逻辑能适配多种不同的数据类型(如整数、字符串、自定义对象等),而无需针对每种类型重复开发
首先我们先看下面的实现交换的函数代码
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++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同 材料的铸件(即生成具体类型的代码)
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础
1.2 泛型编程的核心目标
解决 “相同逻辑需要适配多种数据类型” 的问题,具体目标包括:
- 代码复用:一份逻辑适配多种类型,减少重复代码(避免为
int
、double
、string
等类型分别编写几乎相同的函数或类)。 - 类型安全:在编译期对数据类型进行检查,避免因类型错误导致的运行时问题(优于宏定义等无类型检查的复用方式)。
- 效率无损:通用代码在编译期会被实例化为针对具体类型的专用代码,无额外运行时开销(区别于某些语言的 “动态类型适配”)
二. 模板初阶
C++ 通过模板(函数模板、类模板)实现泛型编程,模板是泛型思想的 “载体”,编译器会根据传入的具体类型,自动生成针对该类型的代码(称为 “实例化”)。
C++中模版分为函数模版和类模版两类
2.1 函数模板(Function Template)
2.1.1 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生 函数的特定类型版本,函数模板是通用的函数定义,可用于处理多种类型的参数。
2.1.2 函数模板的格式
template<typename T1, typename T2, ……,typename Tn>
返回值类型函数名(参数列表){}
// 模板声明:template <模板参数列表>
// template<typename T> // 当前可以理解为class和typename是一样的
template<class T> // T是"类型参数",表示通用类型
void Swap(T& x, T& y) // 函数逻辑与类型T无关
{T tmp = x;x = y;y = tmp;
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替 class)
2.1.3 函数模板的原理
函数模板是一个蓝图,它并不是函数本身,是编译器使用方式产生特定具体函数类型的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演, 将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
下面思考一个问题,下面这个函数调用两个Swap函数,调用的是同一个函数吗?
template<class T>
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}int main()
{int a = 1, b = 2;Swap(a, b);double x = 1.1, y = 2.2;Swap(x, y);return 0;
}
想要更加直观的观察,我们不妨看一下汇编指令
从汇编指令,我们可以清楚地看出它们调用的是不同的函数
注意:当我们在调试时,调用函数时走的都是函数模版,这时为了方便我们调试,并不是说明调用的是同一个函数
2.1.4 函数模板实例化
用不同类型的参数使用函数模板时,编译器会根据实参类型自动推导T的具体类型,并生成对应类型的函数,称为函数模板的实例化。
模板参数实例化分为:隐式实例化 和 显式实例化。
2.1.4.1 隐式实例化
template<class T>
T Add(const T& x, const T& y)
{return x + y;
}int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;// 隐式实例化(通过实参类型,推导模版参数)cout << Add(a1, a2) << endl;cout << Add(d1, d2) << endl;// 该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型// 通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,// 编译器无法确定此处到底该将T确定为int 或者 double类型而报错//Add(a1, d1); // err// 注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化// 下面是使用了第一种方法,自己来强制转换cout << Add(a1, (int)d1) << endl;cout << Add((double)a1, d1) << endl;
}
2.1.4.2 显示实例化:在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& x, const T& y)
{return x + y;
}int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;// 显示实例化cout << Add<int>(a1, d1) << endl;cout << Add<double>(a1, d1) << endl;return 0;
}
// 如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
注意以下场景:编译器推导不出是什么类型是,必须显示实例化
// 显示实例化使用场景
template<class T>
void func(int n)
{T* ptr = new T[n];cout << ptr << endl;
}int main()
{// 这种情况不用显示实例化无法调用// 因为不显示实例化编译器推导不出是什么类型func<int>(10); func<double>(10);return 0;
}
2.1.5 模板参数匹配原则
2.1.5.1 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数// 和模版实例化出的函数构成重载
int Add(int left, int right)
{return left + right;
}// 通用加法函数
template<class T>
T Add(T left, T right)
{return left + right;
}int main()
{int a1 = 10, a2 = 20;double d1 = 10.1, d2 = 20.2;cout << Add(a1, a2) << endl; // // 与非模板函数匹配,编译器不需要特化cout << Add<int>(a1, a2) << endl; // 显示实例化,必须调用模版实例化出的函数cout << Add(d1, d2) << endl; // 调用模版实例化出的函数return 0;
}
2.1.5.2 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}// 通用加法函数
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{return left + right;
}int main()
{cout << Add(1, 2) << endl; // 与非函数模板类型完全匹配,不需要函数模板实例化cout << Add(1, 2.1) << endl; // 模板函数可以生成更加匹配的版本,编译器根据实参生成更匹配的Add函数return 0;
}
2.2 类模板(Class Template)
2.2.1 类模板的定义格式
类模板是通用的类定义,可用于创建支持多种元素类型的类(如容器类)
templateclass T1, class T2, ..., class Tn> class
类模板名
{
// 类内成员定义
};
#include<iostream>
using namespace std;// 类模版
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;
};
注意:模板不能声明和定义分离到 .h 和 .cpp 两个文件中;如果声明和定义分离,必须在同一个文件中。
// 声明和定义分离
// 在同一个文件中声明和定义分离时,比较小的函数可以直接写在类里面,默认为内联;大的函数在类里面声明,在类外实现
// 在类外实现时要指定类域,类域要注意加上模版类型 例:void Stack<T>:: Push()template<class T>
class Stack
{
public:Stack(size_t n = 4);void Push(const T& x);private:T* _a;size_t _top;size_t _capacity;
};template<class T>
Stack<T>::Stack(size_t n):_a(new T[n]), _top(0), _capacity(n)
{
}template<class T>
void Stack<T>::Push(const T& x)
{// 扩容_a[_top++] = x;
}
2.2.2 类模板的实例化
2.2.2.1 类模板在实例化时必须显示实例化
template<class T>
class Stack
{
public:Stack(size_t n = 4);void Push(const T& x);private:T* _a;size_t _top;size_t _capacity;
};template<class T>
Stack<T>::Stack(size_t n):_a(new T[n]), _top(0), _capacity(n)
{
}template<class T>
void Stack<T>::Push(const T& x)
{// 扩容_a[_top++] = x;
}int main()
{// 类模板在实例化时必须显示实例化Stack<int> st1;st1.Push(1);Stack<double> st2;st2.Push(1.1);return 0;
}
2.2.2.2 类模板实例化时可以给缺省值
// 给缺省值
template<class T = int>
class A
{
public:T _a1;T _a2;
};// template<class T1 = int, class T2 = double> // 全缺省
template<class T1, class T2 = double> // 半缺省,必须从右往左给缺省值
class B
{
public:T1 _b1;T2 _b2;
};int main()
{A<> aa1; // 尖括号不能少,不写就用缺省值A<double> aa2;B<int> bb1; // 传参时,从左往右B<int, int> bb2;return 0;
}
注意:
- C++11 及以上:函数模板和类模板的类型参数均可指定缺省值,遵循 “从右向左” 规则,函数模板还可结合类型推导使用。
- C++98:仅类模板支持类型参数缺省值,函数模板不支持。
三. STL 简介
3.1 什么是STL
C++ 中的STL(Standard Template Library,标准模板库) 是 C++ 标准库的核心组成部分,它基于模板技术实现了一套通用的数据结构(容器) 和算法,旨在提高代码复用性、开发效率和程序性能。不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。 2. STL的版本。
STL 的核心目标
- 复用性:提供开箱即用的通用组件(如动态数组、链表、排序算法等),避免重复开发。
- 高效性:组件底层实现经过优化(如基于数据结构理论的最优设计),性能接近手写的专用代码。
- 通用性:通过模板实现与类型无关,支持任意数据类型(基本类型、自定义类等)。
3.2 STL 的版本
1. 原始版本
- Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许 任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原 始版本一样做开源使用。 HP 版本--所有STL实现版本的始祖。
2. P. J. 版本
- 由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读 性比较低,符号命名比较怪异。
3. RW版本
- 由Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一 般。
4. SGI版本
- 由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版 本。被GCC(Linux)采用,可 移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。我们后面学习 STL要阅读部分源代码,主要参考的就是这个版本。
3.3 STL 的六大组件
STL 主要由以下 6 个部分构成,其中容器、算法、迭代器是核心,三者协同工作;其余部分(仿函数、适配器、分配器)是辅助组件。
组成部分 | 作用 |
---|---|
容器(Containers) | 封装数据结构的类模板(如数组、链表、树、哈希表),用于存储数据。 |
算法(Algorithms) | 操作容器中数据的函数模板(如排序、查找、复制),通过迭代器访问容器。 |
迭代器(Iterators) | 连接容器和算法的 “桥梁”,行为类似指针,提供对容器元素的统一访问方式。 |
仿函数(Functors) | 重载() 运算符的类 / 结构体,可作为算法的参数(如自定义比较规则)。 |
适配器(Adapters) | 转换已有组件的接口(如将容器转为栈 / 队列,或修改仿函数的行为)。 |
分配器(Allocators) | 负责容器的内存管理(分配 / 释放),通常无需用户手动干预。 |
结语
如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!