前言
在学习C++的模版之前,咱们先来说一说模版的概念,模版在我们的日常生活中非常常见,比如我们要做一个ppt,我们会去在WPS找个ppt的模版,我们只需要写入内容即可;比如我们的数学公式,给公式套值,就可以算出结果;比如我们在写实验报告,老师会给一个实验报告的模版,我们按照里面的模版直接写入内容即可;所以生活中的模版就是相当于提供了一个事物的框架,我们只需要输入主要的内容就行了。那C++里面的模版也是如此,了解到了这里,就一起开始学习C++的模版吧!
一、函数模版
我们在C++上学过一个函数重载,函数重载就是通过形参类型的不同,他们被分为不同的函数,虽然函数名相同;比如我们要实现一个交换函数,如下代码:
void Swap(int& a, int& b) { int tmp = a; a = b; b = tmp; } void Swap(double& a, double& b) { double tmp = a; a = b; b = tmp; } void Swap(char& a, char& b) { char tmp = a; a = b; b = tmp; }
我们会发现,这作为一个交换函数未免也太麻烦了吧,虽然可以实现不同类型的参数的交换,但是这样的代码多少回=会显得冗余(多余),基于这样的原因,C++创造了函数模版。
1. 函数模版的定义
函数模版代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
如何理解这句话呢?其实就是我给你一个函数的模版,比如交换函数的模版,你只需要填入实参的不同类型,编译器自动识别,给你匹配相应的函数。
其实这属于一种泛型编程,泛型编程指的就是编写与类型无关的通用代码,具有通用性,而模版是泛型编程的基础
2. 函数模版的格式
template 指的是模版,要写函数模版之前,必须要指定模版的参数类型,也就是说T代表的就是模版,T就是可以换成任何数据类型的;而class / typename 是定义模版参数的关键字,可以在<>定义多个模版参数,用逗号分开即可。
下面的代码,就是一个函数模版,我们要实现一个交换函数,那要设计的模版是谁呢?因为不同的类型都要实现交换,所以我们的模版参数要放到形参的位置上,让我们在传不同类型的实参时,可以顺利完成对应的交换。
其中template <class T>中的T是虚拟类型
#include <iostream> using namespace std; template<class T> void Swap( T& left, T& right) { T temp = left; left = right; right = temp; } int main() { int a = 3; int b = 4; Swap(a, b); double c = 2.5; double d = 6.6; Swap(c, d); char ch1 = 'x'; char ch2 = 'y'; Swap(ch1, ch2); cout << a << ' ' << b << endl; cout << c << ' ' << d << endl; cout << ch1 << ' ' << ch2 << endl; return 0; }
在调用函数的时候直接传我们想交换的元素就可以,剩下的都是编译器做的事情。所以我们本来针对不同的数据类型要实现不同的函数重载,但有了函数模版之后,我们就只需要写一个函数了,剩下的是编译器帮我们完成的。不得不感慨一下,真的是懒人创造世界!
3. 函数模版的图示
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供 调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
4. 模版参数的实例化
函数模版的实例化,也就是相当于去调用这个函数模版,我们上面看到的是模版参数是在形参这里的,那如果我们没有在形参中使用模版参数呢?这样如何告诉模版参数是什么数据类型呢?所以在函数模版的实例化分为显式实例化和隐式实例化
4.1 隐式实例化
隐式实例化就是并没有在函数调用的时候,明确指出模版参数的数据类型,比如下面的代码:
#include <iostream> using namespace std; template<class T> void Swap( T& left, T& right) { T temp = left; left = right; right = temp; } int main() { int a = 3; int b = 4; Swap(a, b); return 0; }
4.2 显式实例化
显式实例化,就是在函数调用时,显式的写出了模版参数的数据类型,如下图代码:
#include <iostream> using namespace std; template<class T> void Swap( T& left, T& right) { T temp = left; left = right; right = temp; } int main() { int a = 3; int b = 4; Swap<int>(a, b); return 0; }
大家可能会觉得,这个显式实例化好像挺多余的,因为我们在函数传参的时候就已经隐式的传递了模版参数的数据类型了,那大家再来看下面的代码:
#include <iostream> using namespace std; template<class T> T GetNum(int n) { T b = n; return b; } int main() { int a = 3; int b = 4; int c = GetNum(a); return 0; }
如果我们没办法从传参这里让编译器得知模版参数的数据类型呢?我们这样写是不对的,所以我们不能隐式的传了。必须显式的写,正确的代码如下:
1. #include <iostream> 2. using namespace std; 3. template<class T> 4. T GetNum(int n) 5. { 6. T b = n; 7. return b; 8. }#include <iostream> using namespace std; template<class T> T GetNum(int n) { T b = n; return b; } int main() { int a = 3; int b = 4; int c = GetNum<int>(a); return 0; } 9. int main() 10. { 11. int a = 3; 12. int b = 4; 13. int c = GetNum<int>(a); 14. return 0; 15. }
所以这种情况我们必须显式实例化,<>里面就是告诉模版参数的数据类型。
5. 如何应对不同数据类型的运算
如果我们要实现一个int和double进行加法,我们下面代码还可以实现吗?
#include <iostream> using namespace std; template<class T> T Add(const T& a, const T& b) { return a + b; } int main() { int a = 3; double b = 4.7; cout << Add(a, b) << endl; return 0; }
我们要记住,对于模版函数,不允许类型转换的,所以a是int类型,先传入给T,T就是int类型,又因为不会类型转换,所以会报错的。那应该如何实现呢?我们直接给出最优方案:
#include <iostream> using namespace std; template<class T, class Y> Y Add(const T& a, const Y& b) { return a + b; } int main() { int a = 3; double b = 4.7; cout << Add(a, b) << endl; return 0; }
我们只需要加一个模版参数就可以了,但是要知道返回值的类型必须是类型提升最后的类型,这是什么意思呢?
因为不同类型的运算,会发生类型的提升
char ——> int ——> float ——> double
所以要记住这里就OK了
6. 模版参数的匹配规则
1、 合适匹配的情况下,有现成的就匹配现成的 |
2、没有合适的,就将就用(指没有函数模版的时候) |
3、有更合适就用更合适的,哪怕要自己使用函数模版创造 |
二、类模版
1. 类模版的格式
其实也跟函数模版差不多,就是我们要知道的是,模版参数给谁就行,其他的都不动。
template <class T> class Stack { public: Stack(int capacity = 4) : _a(new T[capacity]) , _size(0) , _capacity(capacity) {} ~Stack() { delete []_a; _a = nullptr; _size = _capacity = 0; } private: T *_a; int _size; int _capacity; };
2. 类模版的类名、类类型和类模版的实例化
这里想说的是,我们定义了类模版之后,类名是什么?类类型是什么?类模版又是如何实例化的呢?
Stack<int> st;
上面的代码是正确调用这个类,那类名还是Stack,而这个类模版的类类型就不是类名类,
而类名+显式实例化模版参数
#include <iostream> using namespace std; template <class T> class Stack { public: Stack(int capacity = 4) : _a(new T[capacity]) , _size(0) , _capacity(capacity) {} ~Stack() { delete []_a; _a = nullptr; _size = _capacity = 0; } private: T *_a; int _size; int _capacity; }; int main() { Stack<int> st; return 0; }
3. 在类模版外的函数定义
这里需要重点注意一下,我们函数定义可以在类内部,也可以在类外部,但是在类外部需要注意一下简单的规则。
1. 类模版外的函数定义,必须在同一个文件里
2. 需要指定域空间和模版
#include <iostream> using namespace std; template <class T> class Stack { public: Stack(int capacity = 4) : _a(new T[capacity]) , _size(0) , _capacity(capacity) {} ~Stack(); private: T *_a; int _size; int _capacity; }; template<class T> Stack<T>::~Stack() { delete []_a; _a = nullptr; _size = _capacity = 0; } int main() { Stack<int> st; return 0; }
三、使用模版的注意事项
1. 模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换;
2. 模板与类型无关,提高了代码复用性;
3. 只要支持模板语法,模板的代码就是可移植的;也就是可移植性好,跨平台性;
4. 类模板是一个类家族,模板类是通过类模板实例化的具体类
5. 类模板的成员函数都是模板函数