目录
一.泛型编程
二.函数模板
1.函数模板语法梳理:
2.函数模板的实例化:
3.函数模板的显式实例化:
4.函数模板使用时的注意事项
三.类模板
1.类模板的语法梳理
2.类模板中声明和定义分离的成员函数
一.泛型编程
泛型编程:编写不依赖于具体数据类型的通用代码,是代码复用的一种手段。
二.函数模板
1.函数模板语法梳理:
C++中可以编写形参类型待定的函数模板。
假如我们现在要设计一个各种类型变量都通用的变量交换函数:
template <typename T1> void swap(T1& Element1, T1& Element2) { T1 tem = Element1; Element1 = Element2; Element2 = tem; }
函数模板书写格式:
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(形参列表){函数体}
- template是定义模板的关键字
- typename是定义模板待定变量类型的关键字
- T1,T2...代表待定类型名,用于作为函数模板的形参类型名
- 注意函数模板本身并不是一个可调用的函数,由函数模板生成可调函数的过程是由编译器在编译阶段完成的
2.函数模板的实例化:
上面代码段中的swap就是一个函数模板,函数模板代表了一个函数家族,swap的形参Element1和Element2的类型都是待定的,实际调用swap函数时:
- 编译器会根据函数调用语句中传入函数的实参的类型生成对应的重载函数(编译器生成模板的重载函数的过程叫做函数模板的实例化),并调用重载函数。
- 也就是说,函数模板转变为可调函数的过程是在代码编译的阶段由编译器根据具体函数调用语句中实参的类型类完成的
- 比如:
template <typename T1> 定义函数模板 void swap(T1& Element1, T1& Element2) { T1 tem = Element1; Element1 = Element2; Element2 = tem; } class Date { public: Date(char year = 0) :_year(year) { cout << "constructor" << endl; } private: char _year; }; int main() { int a = 1; int b = 2; swap(a, b); 交换一对整形变量 double d = 1.0; double c = 2.0; swap(d, c); 交换一对浮点型变量 Date date1('b'); Date date2('c'); swap(date1, date2); 交换一对Date对象 return 0; }
代码编译阶段编译器将函数模板转化为可调函数的过程图解:
- 这种函数模板的实例化方式称为隐式实例化(让编译器自己根据实参推演模板参数的实际类型),函数模板还可以显式实例化。
- 函数重载机制是支持函数模板化编程的底层机制
3.函数模板的显式实例化:
在函数调用语句的函数名后的<>中指定模板参数的实际类型。
template <typename T1> 定义函数模板 void swap(T1& Element1, T1& Element2) { T1 tem = Element1; Element1 = Element2; Element2 = tem; } int main () { double d = 1.0; double c = 2.0; swap<double>(d, c); return 0; }
4.函数模板使用时的注意事项
- 一个非模板函数可以和一个同名的函数模板同时存在。
// 专门处理int的加法函数 int Add(int left, int right) { return left + right; } // 通用加法函数 template<class T> T Add(T left, T right) { return left + right; } void Test() { Add(1, 2); //与非模板函数匹配,编译器不需要特化 Add<int>(1, 2); //调用编译器特化的Add版本(显式实例化模板) }
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。但是如果模板可以产生一个具有更好匹配的函数, 编译器将选择模板实例化函数。
// 专门处理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函数 }
模板实例化函数不能自动对实参进行强制类型转换,但普通函数可以自动对实参进行强制类型转换
三.类模板
1.类模板的语法梳理
类模板的定义格式:
template<typename T1, typename T2, ..., typename Tn> //类模板的未定变量类型列表 class Date //类模板名 { T1 _mem1; //类中未定类型的成员的声明 T2 _nem2; .... };
- T1,T2...具体的类型名在创建类对象时由用户自行指定
比如现在定义一个栈类的模板:
class Date { public: Date(char year = 0) :_year(year) { cout << "constructor" << endl; } private: char _year; }; template <typename T> //Stack类模板 class Stack { public: Stack(int capacity = 4, int nums = 0) //类的构造函数 :_data = nullptr //构造函数初始化列表 , _capacity = capacity , _nums = nums { _data = new T[_capacity]; //为栈申请初始的堆区空间 } ~Stack() //类的析构函数 { delete[] _data; //将栈申请的堆区空间还给系统 _data = nullptr; } private: T* _data; int _capacity; int _nums; }; int main() { Stack<int> a; //创建一个存储int类型数据的栈对象 Stack<double>b; //创建一个存储double类型数据的栈对象 Stack<Date>c; //创建一个存储Date对象的栈对象 return 0; }
注意:
用类模板的方式定义的栈对象使用起来十分方便,可以用于存储各种类型的数据(一般来说实现一个栈类需要上百行代码,实现三种数据类型的栈就要三份代码,然而如果使用模板型编程方式,代码书写量就可以大大减少)
2.类模板中声明和定义分离的成员函数
如果类模板中的成员函数要放在类外进行定义时,需要加上模板参数列表,比如:
template <typename T> class Stack { public: Stack(int capacity = 4, int nums = 0) //类的构造函数 :_data = nullptr //构造函数初始化列表 , _capacity = capacity , _nums = nums { _data = new T[_capacity]; //为栈申请初始的堆区空间 } ~Stack(); //类的析构函数的声明, 将析构函数的定义放在类模板外面 private: T* _data; int _capacity; int _nums; }; template <typename T> Stack<T>::~Stack() //类的析构函数的定义 { delete[] _data; _data = nullptr; }
模板型编程模式可以让代码变得更简洁,简洁的代码维护起来才更加的方便高效。