在 C
语言中,如果我们想写多类型的,并且是同一个函数出来的函数,我们只能要几个写几个出来,这样子会显得比较冗余,也加大了程序员的代码量,于是 c++
中就引入了 函数重载 和 泛型编程 的概念,大大的简化了我们的工作!
仅靠函数重载是完不成泛型编程的需求的,比如说下面的代码:
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
void Swap(double& x, double& y)
{
double tmp = x;
x = y;
y = tmp;
}
void Swap(char& x, char& y)
{
char tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0, b = 1;
double c = 1.1, d = 2.2;
char e = 'a', f = 'b';
Swap(a, b);
Swap(c, d);
Swap(e, f);
return 0;
}
好像靠函数重载来调用不同类型的 Swap
,只是表面上看起来 “通用” 了 ,实际上问题还是没有解决,有新的类型,还是要添加对应的函数。
用函数重载解决的缺陷:
泛型编程:编写与类型无关的通用代码,是 代码复用的一种手段。而模板是泛型编程的基础。
其中,模板分为两类,一类是 函数模板,一类是 类模板。
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
template<class T1, class T2, class T3, ........., class Tn>
返回值类型 函数名(参数列表) {函数体}
① template 是 定义模板 的关键字,后面跟着尖括号 <>
② class 是用来 定义模板参数 的关键字 (也可以用 typename
)
③ T1
, T2
, …, Tn
表示的是函数名,可以理解为模板的名字,名字你可以自己取。
template<typename T> // 模板参数列表 ———— 参数类型
void Swap(T& x, T& y) // 函数参数列表 ———— 参数对象
{
T tmp = x;
x = y;
y = tmp;
}
📌注意事项:
函数模板不是一个函数,因为它不是具体要调用的某一个函数,只是一副蓝图。就像 “好学生”,主体是学生,“好” 是形容 “学生” 的;这里也一样,“函数模板” 是模板,所以函数模板表达的意思是 “函数的模板” 。
所以,我们一般不叫它模板函数,应当叫作函数模板。而 模板函数是一种用模板实例化出来的函数。
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实 模板就是将本来应该我们做的重复的事情交给了编译器去做。
在编译器 编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用 double
类型使用函数模板时,编译器通过对实参类型的推演,将 T
确定为 double
类型,然后产生一份专门处理 double
类型的代码,对于字符类型也是如此。
通俗易懂的说,就是这里三个不同参数类型函数,不是同一个函数,我们只负责传类型参数,而生成这些函数的工作由编译器替我们做!
用不同类型的参数使用模板参数时,称为函数模板的实例化。其中模板参数实例化分为:隐式实例化 和 显式实例化 ,下面我们来分别讲解一下这两种实例化。
📚 定义:让编译器根据实参,推演模板函数的实际类型。
以下面 add
函数代码为例子:
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
return 0;
}
// 运行结果:
30
30.3
现在思考一个问题,如果出现 int + double
不同类型相加这种情况呢❓ 实例化能成功吗❓
Add(a1, d2);
最后程序报错了,也就是编译器无法根据一个 T
来推出两个类型想要用哪个。
💡 解决方法:
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.1, d2 = 20.2;
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
cout << Add((double)a1, d2) << endl; // 将a1强转为double,或者将d2强转为int都行
return 0;
}
2. 写两个模板参数,其中返回的参数类型起决定性作用:
template<class T1, class T2>
T1 Add(const T1& x, const T2& y) // 那么T1就是int,T2就是double
{
return x + y; // 范围小的会向范围大的提升,也就是int会转为double
} // 最后表达式会是一个double,但是最后返回值又是T1,是int,又会转
int main(void)
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, d2) << endl; // int,double 👆
return 0;
}
3. 使用 “显式实例化” 来解决:
Add<int>(a1, d2); // 指定实例化成int
Add<double>(a1, d2) // 指定实例化成double
📚 定义:在函数名后的 <>
里指定模板参数的实际类型。
简单来说,显式实例化就是在中间加一个尖括号 <>
去指定你要实例化的类型。(在函数名和参数列表中间加尖括号)
函数名 <类型> (参数列表);
所以对于上面的问题,我们可以用该方法解决:
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
cout << Add<int>(a1, d2) << endl; // 指定T用int类型
cout << Add<double>(a1, d2) << endl; // 指定T用double类型
return 0;
}
// 运行结果:
30
30.3
30
30.2
🔑 解读:
像第一个 Add<int>(a1, a2)
,a2
是 double
,它就要转换成 int
。第二个 Add<double>(a1, a2)
,a1
是 int
,它就要转换成 double
。像这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。像 double
和 int
这种相近的类型,是完全可以通过隐式类型转换的。
🔺总结:
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数
int Add(int x, int y) {
return x + y;
}
// 通用加法函数
template<class T>
T Add(const T& x, const T& y) {
return x + y;
}
int main()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
return 0;
}
对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数,而不会从该模板生成一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
🔺总结:
template <class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
}
有了类模板,我们就有得天独厚的优势:不用再去 typedef
类型!
我们之前 C
语言中对于一些数据结构比如 stack
,我们在设置 int
类型的时候,只能去 typedef
为 int
,然后如果需要 double
则重新去 typedef
为 double
,非常麻烦,而且这种方式做不到同时申请一个 int
类型和 double
类型的 stack
。
现在有了类模板,我们只需要在定义声明时候设置它的格式即可!
// 注意:Stack不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Stack
{
// 类内成员定义
};
int main()
{
Stack<int> st1; // 存储int
Stack<double> st2; // 存储double
return 0;
}
🍡 注意事项:这里的 Stack
不是具体的类,是编译器根据被实例化的类型生成 具体类的模具。
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟 <>
,然后将实例化的类型放在 <>
中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
类名<类型> 变量名;
// 例如:
Stack<int> st1;
Stack<double> st2;
🍡 注意事项:这里的 Stack
是 类名,而 Stack<int>
是 类型。
如果我们在类外定义函数时候这样子定义的话是会报错的:
template<class T>
class Stack {
public:
Stack(T capacity = 4)
: _top(0)
, _capacity(capacity)
{
_arr = new T[capacity];
}
~Stack(); // 这里我们让析构函数放在类外定义
private:
T* _arr;
int _top;
int _capacity;
};
/* 类外 */
Stack::~Stack() { ❌ // 即使是指定类域也不行
...
}
🔑 解答:
① Stack
是类名,Stack<int>
才是类型。这里要拿 Stack<T>
去指定类域才对。
② 类模板中的函数在类外定义,没加 “模板参数列表” ,编译器不认识这个 T
。类模板中函数放在类外进行定义时,需要加模板参数列表。
💬 代码演示: 我们现在来看一下如何添加模板参数列表!
template<class T>
class Stack {
public:
Stack(T capacity = 4)
: _top(0)
, _capacity(capacity) {
_arr = new T[capacity];
}
// 这里我们让析构函数放在类外定义
~Stack();
private:
T* _arr;
int _top;
int _capacity;
};
// 类模板中函数放在类外进行定义时,需要加模板参数列表
// Stack是类名,而Stack<T> 才是类型
template <class T>
Stack<T>::~Stack()
{
delete[] _arr;
_arr = nullptr;
_capacity = _top = 0;
}
class
和 typename
来定义模板,如 template<class T1, typename T2>
。Nontype Template Parameters
)💭 举例: 假设我们要定义一个静态的数组:
#define X 1000
template<class T>
class Srray
{
T _arr[X];
};
int main()
{
Srray<int> a1;
Srray<int> a2;
return 0;
}
这个时候我们发现,这个静态数组的大小是固定死的,我们做不到让 define
运行时候改变数值呀,如果我想让 a1
开 1000
个空间,让 a2
开 100
个,那对于 a2
来说,是不是就浪费了 900
个空间了,那这个时候应该怎么做呢?
这个时候就引入了这个非类型模板参数!
注意看,我们普通定义的 T
是类型,而 N
这里并不是类型,而是一个常量!类型模板参数定义的是虚拟类型,注重的是你要传什么,而 非类型模板参数定义的是常量。
"非类型模板参数"
👇
template<class T, size_t N> class array;
👆
"类型模板参数"
模板参数分为类型形参与非类型形参:
class
或者 typename
之类的参数类型名称。 既然有了这个非类型模板参数,我们尝试着来解决问题!
//#define X 1000
template<class T, size_t X>
class Srray
{
T _arr[X];
};
int main()
{
Srray<int, 100> a1; // 开辟了100个空间大的静态数组
Srray<int, 1000> a2; // 开辟了1000个空间大的静态数组
return 0;
}
这里我们在模板这定义一个常量 X
,让它去做数组的大小。
于是我们就可以在实例化 Srray
的时候指定其实例化对象的大小了,分别传 100
和 1000
。
📌 注意事项:
① 非类型模板参数是 整形常量,是不能修改的。
template<class T, size_t X>
class Srray
{
public:
void Change()
{
X = 10;
}
private:
T _arr[X];
};
int main()
{
Srray<int, 100> a1; // 开辟了100个空间大的静态数组
Srray<int, 1000> a2; // 开辟了1000个空间大的静态数组
a1.Change();// 编译器会报错,因为X是常量,不能修改
return 0;
}
② 有些类型是不能作为非类型模板参数的,比如浮点数、类对象以及字符串,也就是说,只能用 size_t
、int
、char
(char
也算整型)
template<string S> ❌
class A
{
// 类内成员定义
};
template<double D> ❌
class B
{
// 类内成员定义
};
③ 非类型的模板参数必须是在编译期就能确认结果的。
🔍 官方文档:array - C++ Reference
现在学了非类型模板参数了,我们现在再来回头看 array
:
array
是 C++11
新增的,它有什么独特的地方吗?很可惜,基本没有,并且 vector
可以完全碾压 array
。
#include <iostream>
#include <array>
#include <vector>
using namespace std;
int main()
{
vector<int> v1(100, 0);
array<int, 100> a1;
cout << "size of v1: " << sizeof(v1) << endl;
cout << "size of a1: " << sizeof(a1) << endl;
return 0;
}
// 🚩 运行结果:
size of v1: 32
size of a1: 400
vector
是开在空间大的堆上的,而 array
是开在寸土寸金的栈上的,堆可比栈的空间大太多太多了。
最尴尬的是 array
能做的操作几乎 vector
都能做,因为 vector
的存在 array
显得有些一无是处。
比起原生数组,array
的最大优势也只是有一个越界的检查,读和写都可以检查到是否越界。要对比的话也只能欺负一下原生数组,然而面对强大的 vector
,array
完全没有招架之力。
🔺 总结: array
相较于原生数组,有越界检查之优势,实际中建议直接用 vector
。
有时候,编译默认的函数模板和类模板(在一些特殊场景下)不能正确处理需要逻辑,这时候就需要针对这些指定类型进行特殊化处理,所以就要做模板的特化。
💭 举例①: 字符串类型的比较
template<class T>
bool IsSame(const T& a, const T& b)
{
return a == b;
}
int main()
{
// 对于一般的类型对比,没有问题
cout << IsSame(1, 2) << endl;
cout << IsSame(1.1, 2.2) << endl;
// 这里好像就出问题!
char s1[] = "liren";
char s2[] = "liren";
cout << IsSame(s1, s2) << endl;
const char* s3 = "lirendada";
const char* s4 = "lirendada";
cout << IsSame(s3, s4) << endl;
return 0;
}
原因如下图所示:
但也不对啊,字符串我们比较的都是字符大小,而不是比较地址,所以出现了这种情况,模板也没办法帮我们解决,所以我们就得来自己动手特化!
💭 举例②: 自定义类型的比较
#include "Date.h" /* 引入自己实现的日期类 */
/* 判断左数是否比小于右数 */
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // 可以比较,结果正确
Date* p1 = new Date(2022, 7, 16);
Date* p2 = new Date(2022, 7, 15);
cout << Less(p1, p2) << endl; // 可以比较,结果正确
return 0;
}
❓运行结果:(我们运行几次发现,其结果不稳定,对于 Date*
一会是真一会是假)
上述示例中,p1
指向的 d1
显然小于 p2
指向的 d2
对象,但是 Less
内部并没有比较 p1
和 p2
指向的对象内容,而比较的是 p1
和 p2
指针的地址,这就无法达到预期而错误。
此时,就 需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为 函数模板特化与类模板特化。
函数模板的特化有两种方法:
template
后面接上一对空的尖括号 <>
。 直接写出需要的函数以及类型,也就是直接写出重载的函数(比较简单一点)。
例子①: 对于字符串的特化
#include<cstring>
template<class T>
bool IsSame(const T& a, const T& b)
{
return a == b;
}
// 方法一:标准的特化写法
template<>
bool IsSame<const char*>(const char* const& a, const char* const& b)
{
return strcmp(a, b) == 0;
}
// 方法二:字符数组类型的特化
bool IsSame(char* a, char* b)
{
return strcmp(a, b) == 0;
}
// 方法二:字符串常量类型的特化
bool IsSame(const char* a, const char* b)
{
return strcmp(a, b) == 0;
}
int main()
{
// 对于一般的类型对比,没有问题
cout << IsSame(1, 2) << endl;
cout << IsSame(1.1, 2.2) << endl;
char s1[] = "liren";
char s2[] = "liren";
cout << IsSame(s1, s2) << endl;
const char* s3 = "lirendada";
const char* s4 = "lirendada";
cout << IsSame(s3, s4) << endl;
return 0;
}
// 🚩 运行结果:
0
0
1
1
❓ 问题:为什么标准特化写法那里的参数类型是 const char* const& a
,而不是 const char*& a
?
💡 解答:
因为原来的模板参数的里面是 const T& a
,而这里的 const
是用来修饰 a
的,我们特化后仅仅把 T
特化为 const char*
,但是原来的类型是 const &
的,根据上面的特化要求第四点,函数形参表必须要和模板函数的基础参数类型完全相同,所以特化后的参数类型必须为将 const &
带上,最后就变成 const char* const& a
。
也注意不要太抬杠,毕竟语法是被人设定出来,这很大程度取决于设计人当时的想法!
例子②: 对于日期类等自定义类型
#include <iostream>
#include "Date.h"
using namespace std;
// 必不可少的原版
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 针对某些类型要特殊化处理 ———— 使用模板的特化解决
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
// 直接匹配的普通函数
bool Less(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
Date d1(2022, 7, 7); //变量开辟在栈区
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl;
Date* p1 = new Date(2022, 7, 16); //动态开辟,在堆区
Date* p2 = new Date(2022, 7, 15);
cout << Less(p1, p2) << endl;
return 0;
}
// 🚩 运行结果:
1
1
0
💡 解读: 对于普通类型,它还是会调正常的模板。对于 Date*
编译器就会发现这里有个专门为 Date*
而准备的特化版本,编译器会优先选择该特化版本。
❓ 思考:现在我们加一个普通函数,Date*
会走哪个版本?
// 原模板
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 对模板特化的
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
// 直接匹配的普通函数
bool Less(Date* left, Date* right)
{
return *left < *right;
}
🔑 答案: 函数重载,会走直接匹配的普通函数版本,因为有现成的,就不用实例化了。
函数模板不一定非要特化,因为在参数里面就可以处理,写一个匹配参数的普通函数也更容易理解。该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化,直接给出匹配参数的普通函数即可。
刚才函数模板不一定非要特化,因为可以写一个具体实现的函数。但是 类模板我们没法实现一个具体的实际类型,就必须要特化。
假设针对模板参数 T1
和 T2
分别是 int
和 int
,想对这种特定类型做出特殊化处理,该怎么办,如下所示:
#include <iostream>
#include "Date.h"
using namespace std;
template<class T1, class T2>
class Data
{
public:
fun() {
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
int main()
{
Data<int, int> d1; // 想要对其进行特殊化处理
Data<int, double> d2;
return 0;
}
此时特化的方法很简单:增加一个对 int
和 int
的特化处理:
template<>
class Data<int, int>
{
public:
fun() {
cout << "Data<int, int>" << endl;
}
}
有了这个特化后,我们的 d1
就会去我们特化的版本完成定义,而 d2
仍然是去我们的原模板完成定义。
🚩 运行结果:
类模板的特化分为两种:全特化 和 偏特化(也称为半特化)
全特化:将模板参数列表中所有的参数都确定化。比如我们上面写的 Data
的特化就是全特化版本的:
template<>
class Data<int, int> // 参数列表中的所有参数都是确定的
{
public:
fun() {
cout << "Data<int, int>" << endl;
}
}
半特化: 任何针对模版参数进一步进行条件限制设计的特化版本,将部分参数类表中的一部分参数特化。(半特化并不是特化一半,就像半缺省并不是缺省一半一样)
比如对于以下的模板类:
template<class T1, class T2>
class Data
{
public:
Data() { cout<<"Data<T1, T2>" <<endl; }
private:
T1 _d1;
T2 _d2;
};
偏特化有以下两种表现方式:
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data() {
cout<< "Data<T1, int>" <<endl;
}
};
// 两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data() {
cout<<"Data<T1*, T2*>" <<endl;
}
};
// 两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data() {
cout<<"Data<T1&, T2&>" <<endl;
}
};
void test2 ()
{
Data<double , int> d1; // 调用特化的int版本
Data<int , double> d2; // 调用基础的模板
Data<int *, int*> d3; // 调用特化的指针版本
Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}
// 🚩 运行结果:
Data<T1, int>
Data<T1, T2>
Data<T1*, T2*>
Data<T1&, T2&>
有如下专门用来按照小于比较的类模板 Less
:
#include<vector>
#include <algorithm>
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 6);
Date d3(2022, 7, 8);
vector<Date> v1;
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 可以直接排序,结果是日期升序
sort(v1.begin(), v1.end(), Less<Date>());
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 可以直接排序,结果错误,日期还不是升序,而v2中放的地址是升序
// 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
// 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
sort(v2.begin(), v2.end(), Less<Date*>());
return 0;
}
通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指针,结果就不一定正确。因为:sort
最终按照 Less
模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题:
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return (*x) < (*y);
}
};
特化之后,在运行上述代码,就可以得到正确的结果!
C++
的标准模板库(STL
)因此而产生。 假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// Add.h文件
template<class T>
T Add(const T& left, const T& right);
// Add.cpp文件
#include "Add.h"
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp文件
#include "Add.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
简单来说,就是模板分离编译的话,定义模板的地方只有定义没有实例化,而实例化的地方只有声明没有定义。
💡 解决方法:
xxx.hpp
里面或者 xxx.h
其实也是可以的。推荐使用这种。将模板的实现和声明都放在头文件中,这样在使用模板的时候,编译器能够立即看到模板的实现细节,并生成对应的模板实例化代码。这种方法适用于模板实现代码比较简单的情况,可以避免模板分离编译带来的问题。【分离编译扩展阅读】 http://blog.csdn.net/pongba/article/details/19130