1、模板的概念
模板:模板就是建立通用的模具,大大提高复用性
- C++另一种编程思想称为泛性编程,主要利用的技术就是模板
- C++提供两种模板机制:函数模板和类模板
2、函数模板
函数模板语法:
template<typename T>//声明创建模板
函数声明或定义
解释:
template——声明创建模板
tepename——表明其后面的符号是一种数据类型,可以用class代替
T——通用的数据类型,名称可以替换,推出为大写字母
函数模板作用:
建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。
(将返回值类型和参数类型抽象出来通用)
案例:交换两个数据的函数
不利用模板的话需要编写两个交换函数
两个交换函数只有数据类型不同,可以利用函数模板
(1)创建函数模板
(声明一个函数模板,告诉编译器后面代码中紧跟着的T不要报错,T是一个通用数据类型)
(2)使用函数模板
两种使用方式:
1. 自动类型推导
(编译器根据传入的数据类型来推导)
2. 显示指定类型
(函数后加上<>告诉编译器传入的数据类型)
3、函数模板注意事项
- 自动类型推导,必须推导出一致的数据类型T才可以使用
- 模板必须要确定出T的数据类型,才可以使用
(如不能输出一句话,必须确定T的数据类型)
案例:数组排序
#include<iostream>
using namespace std;
template<class T>
void mySort(T arr[],int len;i++)
{
for(int i=0;i<len;i++){
max=i;
for(int j=i+1;j<len;j++){
if(arr[max]<arr[j]){
max=j;
}
}
if(max!=i){
int temp=max;
max=i;
i=temp;
}
}
}
//打印模板
template<class T>
void printArray(T arr[],int len)
{
for(int i=0;i<len;i++){
cout<<arr[i]<<" ";
cout<<endl;
}
}
void test01()
{
//测试数组
char charArr[]="bsjg";
int num=sizeof(charArr);
mySort(charArr,num);
printArray(charArr,num)
}
void test02()
{
int intArr[]={8,7,1,5,3,6,8};
int num=sizeof(intArr);
mySort(intArr,num);
printArray(intArr,num);
}
int main()
{
system("pause");
return 0;
}
4、普通函数与模板函数的区别
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
示例:
普通函数可以发生隐式转换,将字符c转换成ASCII码表中的int
函数模板调用利用自动类型推导,不会发生隐式类型转换,不会将字符型转为整型编译器报错
函数模型利用显示类型推导在<>中输入int可以发生隐式类型转换,可将字符转换为整型
总结:
建议使用显示指定类型的方式调用函数模板,这样就确定了通用类型T,以免编译器自动推导发生错误
5、普通函数与函数模板调用规则
规则:
- 如果函数模板和普通函数都可以实现,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板
- 函数模板也可以发生重载
- 如果函数模板可以产生更好的匹配,优先调用函数模板
示例:
1.如果函数模板和普通函数都可以实现,优先调用普通函数
(允许函数模板和普通函数同时存在)
如果调用该函数调用的则是普通函数
2.可以通过空模板参数列表来强制调用函数模板
myPrint<>(a,b);//空模板参数列表
(调用时加上<>可以强制调用模板函数)
3.函数模板也可以发生重载
(变为三个参数,两个参数的函数和三个函数的参数之间,发生函数重载)
(如上图,含有三个参数的函数调用,调用的就是重载后的函数模板)
4.如果函数模板可以产生更好的匹配,优先调用函数模板
(传入的是两个字符型数据,若调用普通函数需要发生隐式类型转换,而调用函数模板则不需要发生隐式类型转换,直接推导T为字符型就可以,更简单,所以编译器调用的是模板函数)
总结:
既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性
5.模板的局限性
局限性:
模板的通用性并不是万能的
(如果传入的数据为数组,则无法实现赋值的功能,因为数组本来就不行直接复制,需要利用循环语句赋值)
(如一个自定义的数据类型,这个类内有许多属性,如将这两个数据进行比较,编译器无法判断用哪一个属性进行比较)
因此C++为了解决这些问题提供了模板的重载,可以为这些特定的类型提供具体化的模板
(无法对比两个person类型的数据)
解决:
1.重载运算符
2.重载模板(具体化模板)
(重载之后如果传入的是person类型的数据,调用的则是重载后的模板函数)
总结:
- 利用具体化的模板(重载的模板)可以解决自定义类型的通用化
- 学习模板并不是为了写模板,而是在STL能够运用系统提供的模板
6、类模板
类模板作用:
建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟的类型来代表
(类中属性的数据类型不确定,等待使用实确定)
语法:
template<typename T>
类
解释:
template——声明创建模板
typename——表明其后面的符号是一种数据类型,可以用class代替
T——通用的数据类型,名称可以替换,通常为大写字母
示例:
(如果有多个数据类型可以使用多个通用的数据类型,不一定要使用T)
(<>内传入数据类型,后创建对象,再传入实参)
7、类模板与函数模板的区别
类模板和函数模板的主要区别:
- 类模板没有自动类型推导的使用方式
- 类模板在模板参数列表中可以有默认参数
示例:
- 类模板没有自动类型推导的使用方式
(必须在<>内显示指定类型)
- 类模板在模板参数列表中可以有默认参数
(函数模板中不可以有默认参数)
在类模板的参数列表中设置默认参数,在使用时可以不指定该参数的类型
8、类模板中成员函数的创建时机
类模板中的成员函数和普通类中的成员函数创建时机是有区别的:
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才能创建
示例:
在类模板中创建成员函数由于无法确定数据类型,funct1和func2无法被成功创建
在调用时确定了数据类型才能被调用
两种数据类型的函数都可以写进MyClass这个类内,因为这是一个类模板,数据类型待定,直到调用时用显示指定数据类型确定了数据类型,才能调用MyClass这个类内的相应数据类型的函数
9、类模板对象做函数参数
类模板实例化出的对象,向函数传参的方式
三种传入方式:
- 指定传入的类型——直接显示对象的数据类型(最常用)
- 参数模板化——将对象中的参数变为模板进行传递(类模板与函数模板配合)
- 整个类模板化——将这个对象类型模板化进行传递
示例:
目的:
让p这个类模板下的对象作为一个实参(Person<string,int>&p)入函数printperson,调用showperson这个函数
template<class T1,class T2>
class Person
{
public:
Person(T1 name,T2 age)
{
//构造函数
//将属性初始化
this->m_Name=name;
this->m_Age=age;
}
void showperson1()
{
cout<<"姓名"<<this->m_Name<<"年龄"<<this->m_Age<<endl;
}
//T1T2两种数据类型的数据
T1 m_Name;
T2 m_Age;
};
//1.指定传入类型
void printperson1(Person<string,int>&p)
{
p.showperson();
}
void test01()
{
Person<string,int>p("小白",3);
printperson1(p);
//指定了T1T2为tring,int
//Person<string,int>为p的数据类型
}
//2.参数模板化
//不指出参数的数据类型而是用T1,T2代替
//要注意编译器无法识别T1,T2,需要加上模板说明这是两个数据类型
template<class T1,classT2>
void printperson2(Person<T1,T2> &p)
{
p.showperson();
//利用typeid查看编译器推出的T1T2类型
cout<<"T1的类型为"<<typeid(T1).name()<<endl;
cout<<"T2的类型为"<<typeid(T2).name()<<endl;
}
void test02()
{
Person<string,int>p("小猪",5);
printperson2(p);
}
//3.整个类模板化
//整个T就是一个person数据类型
template<class T>
void printperson3(T &p)
{
p.showperson();
cout<<"T的类型为"<<typeid(T).name()<<endl;
}
void test02()
{
Person<string,int>p("小李",5);
printperson3(p);
}
利用typeid查看编译器推出的T1T2类型
2.参数模板化
将传入参数的数据类型用 T1T2表示
3.整个类模板化
将整个person类用T表示
10、类模板与继承
当类模板遇到继承时需要注意:
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
- 如果想灵活指定父类中T的类型,子类也虚变为类模板
示例:
按常规方法继承,因为数据类型不确定,编译器无法给从父类中继承下的数据分配内存,编译器报错
正确的书写:
在继承的父类后加<>,在<>中指明数据类型
但是这样修改后父类中的数据类型只能是int类型
如果想要灵活指定父类中的数据类型,子类也需要变类模板,将int写为通用数据类型T(注意此时子类与父类都是类模板,需要区分两个T)这样继承下来的也是一个通用数据类型,可以在使用时实例化对象再确定从父类中继承下来的属性的数据类型
(相当于int,和char也像一个参数一样传递给T1,T2)
11、类模板成员函数的类外实现
类内声明
类外实现
注意:
1.加作用域
(到这一步只是一个普通的成员函数类外实现)
2.在作用域名称后加<T1,T2>,来表明Person是一个类,该实现是类模板成员函数的类外实现
3.声明T1,T2
12、类模板分文件编写
问题:
类模板中成员函数创建时机是在调试阶段,导致份文件编写时链接不到
解决:
方式1:
直接包含.cpp源文件
方式2:(常用)
将声明和实现写到同一个文件中,并改后缀名.hpp,hpp是约定名称,不是强制
示例:
方式1:
将常规的头文件
#include "person.h"
改为直接包含.cpp源文件
#include "preson.cpp"
(因为类模板中的成员函数不是一开始就创建的,是调试时才创建的,导致无法调用,但包含cpp之后相当于直接让编译器查看cpp中的函数实现)
13、类模板和友元
掌握类模板配合友元函数的类内和类外实现
全局函数类内实现——直接在类内声明友元即可(建议使用)
全局函数的类外实现——需要提前让编译器知道全局函数的存在
示例:
注意:将函数写在类内,但是没有写在权限下也属于全局函数
全局函数类内实现:
直接加上关键字friend
全局函数类外实现:
类内声明:
加关键字friend来表示该函数为
加空模板参数列表,来表示这是一个函数模板的函数声明
类外实现:
将实现代码写在代码的最开始
并且让编译器知道Person类的存在
因为Person是类模板,所以也需要提前声明通用数据类型