在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
返回值类型 函数名(参数列表)
{}
① template 是定义模板的关键字,后面跟的是尖括号 < >
② class 是用来定义模板参数的关键字**(也可以用typename,但是STL中多用class)**
③ 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
❓ 现在思考一个问题,如果出现 a1 + d2 这种情况呢?实例化能成功吗?
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
这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。像 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
如果我们在类外定义函数时候这样子定义的话是会报错的:
template<class T>
class Stack {
public:
Stack(T capacity = 4)
: _top(0)
, _capacity(capacity) {
_arr = new T[capacity];
}
// 这里我们让析构函数放在类外定义
void Push(const T& x);
~Stack();
private:
T* _arr;
int _top;
int _capacity;
};
/* 类外 */
Stack::~Stack() { ❌ // 即使是指定类域也不行
...
}
解答:
① Stack 是类名,Stack
② 类模板中的函数在类外定义,没加 “模板参数列表” ,编译器不认识这个 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;
};
// 类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Stack<T>::~Stack() { // Stack是类名,不是类型! Stack 才是类型,
delete[] _arr;
_arr = nullptr;
_capacity = _top = 0;
}
① 模板可以具有 非类型参数,用于指定大小,可以根据指定的大小创建动态结构(这个下面会讲到)。
② 模板 运行时 不检查数据类型,也不保证数据安全,相当于类型的宏替换。
③ 类模板 是一个类家族,而 模板类 是通过 类模板实例化的具体类。
④ 类模板中的 成员函数 全是模板函数,在类外定义时都必须通过完整的模板语法进行定义。
举例: 假设我们要定义一个静态的数组:
#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
#include
#include
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指针的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
函数模板的特化有两种方法:
直接写出需要的函数以及类型,也就是直接写出重载的函数(比较简单一点)。
例子①: 对于字符串
#include
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 &* ?
解答:
因为原来的模板参数的里面是 const T& a,而这里的 const 是用来修饰 a 的,我们特化后仅仅把 T 特化为 const char*,但是原来的类型是 const & 的,根据上面的特化要求第四点,函数形参表必须要和模板函数的基础参数类型完全相同,所以特化后的参数类型必须为将 const & 带上,最后就变成 const char* const& a 。
例子②: 对于自定义类型
#include
#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
bool Less(T left, T right) {
return left < right;
}
// 对模板特化的
template<>
bool Less(Date* left, Date* right) {
return *left < *right;
}
// 直接匹配的普通函数
bool Less(Date* left, Date* right) {
return *left < *right;
}
答案: 函数重载,会走直接匹配的普通函数版本,因为是现成的,不用实例化。
函数模板不一定非要特化,因为在参数里面就可以处理,写一个匹配参数的普通函数也更容易理解。该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化,直接给出匹配参数的普通函数即可。
刚才函数模板不一定非要特化,因为可以写一个具体实现的函数。但是类模板我们没法实现一个具体的实际类型,就必须要特化了。
假设针对 T1 和 T2 是 int 和 int,想对这种特定类型做出特殊化处理,该怎么办?:
#include
#include "Date.h"
using namespace std;
template<class T1, class T2>
class Data
{
public:
fun()
{
cout << "Data" << 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" << endl;
}
}
有了这个特化后,我们的 d1 就会去我们特化的版本完成定义,而 d2 仍然是去我们的原模板完成定义。
类模板的特化分为两种:全特化 和 偏特化(也称为半特化)
全特化:全特化即是将模板参数列表中所有的参数都确定化。我们上面写的 Data 的特化就是全特化版本的:
template<>
class Data<int, int> //参数列表中的所有参数都是确定的
{
public:
fun()
{
cout << "Data" << endl;
}
}
半特化: 任何针对模版参数进一步进行条件限制设计的特化版本。将部分参数类表中的一部分参数特化。(半特化并不是特化一半,就像半缺省并不是缺省一半一样)
比如对于以下的模板类:
template<class T1, class T2>
class Data
{
public:
Data() {cout<<"Data" <<endl;}
private:
T1 _d1;
T2 _d2;
};
偏特化有以下两种表现方式:
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data()
{
cout<<"Data" <<endl;
}
};
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data()
{
cout<<"Data" <<endl;
}
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data()
{
cout<<"Data" <<endl;
}
};
void test2 ()
{
Data<double , int> d1; // 调用特化的int版本
Data<int , double> d2; // 调用基础的模板
Data<int *, int*> d3; // 调用特化的指针版本
Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}
运行结果:
Data
Data
Data
Data
有如下专门用来按照小于比较的类模板Less:
#include
#include
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