模板引入
模板主要是用来解决类库与类库使用不是一个人,编写类库时,不能确定使用类库的人会传入什么类型的数据,而做的一个类型抽象,编译器根据具体调用类库的时候,调用者传入的参数自动推导出数据类型,来替换类库中的数据类型。template关键字定义一个模板,typename关键字指定模板类型抽象名称。示例如下:
template<typename T> void myswp(T &a,T &b) //交换数据
{
T temp;
temp=a;
a=b;
b=temp;
}
int a = 3;
int b = 2;
myswp(a,b);//调用模板函数,模板根据a,b类型推导出模板类型;
模板函数,就是在编写函数时不指定形参具体类型,而是由编译器根据调用函数时传参类型来倒推函数(形参)的类型。类型和值都参数化,对类型和值都延迟确认;最终生成的可执行文件无模板痕迹,在编译时就已经把模板处理好了,模板是c++的一种语法糖,目的是减少编程中代码重写这种体力活的一种语法特性。
模板编程也是一种多态的表现,继承中的多态是“动态多态”,而模板是“静态多态”,继承中的多态是指运行目标在代码运行过程中根据输入条件来确定如何运行,而静态多态是编译器在编译过程中,根据编译时的状态来确定如何编译,运行过程是不可变,所以叫静态。
模板有什么用
模板在c++中的地位就类似于指针在c中的地位,会模板的人与不会模板的人完全是两重天,模板能让我们编写与类型无关的函数,动态确定类型,模板是一种抽象维度,使用模板能让我们写出抽象度更高的代码,模板非常适合用来写库函数,因为它的数据类型可以在执行的时候来确定。使得模板的通用型特别高,抽象是拿复杂度来节省工作量,用处很大,但是学起来也更难。
模板的2个细节
- 如上例,模板定义时使用的typename也可以使用class替换,效果完全没差别,使用谁看个人习惯,如下示例,注意定义模板时后面不能有封号。
template <tepename T>等效于template<class T> //后面无“ ; ”封号
- 模板定义中可以有多个tepename,使用时按名称匹配,用来给函数同时传入不同类型的数,示例如下:
template <typename T,typename A> //定义2个模板名称
T myswp(T &a ,A &b) //传参使用不同类型,返回值使用模板类型
模板名称可以任意取,只是行业习惯使用T。
类模板
模板分2种,函数模板和类模板,前面我们引入的就是函数模板,即在函数的形参或返回值使用模板的虚拟类型。而类模板实际上和函数模板也很类似,目的就是将类中的一些类型使用模板定义成虚拟类型,使用类的人在定义对象的时候再实际传入模板的类型。类模板示例如下:
template <typename T> class people //模板类前添加模板类型声明
{
public:
people(T a); //类型使用模板虚拟类型//是people<T>(T a)简写
private:
T age;
};
template <typename T> people<T>::people(T a) //外置带参构造函数
{
age=a;
}
int main(void)
{
people<int> p(5); //使用类须指定模板的类型,定义一个对象
}
语法特点
- 定义类的时候class前面使用template<typename T>声明类中的虚拟类型。作用域仅是当前代码段,如下示例:
template<typename T> class a //作用域在大括号内有效,大括号在类或函数常见
{
}
- 外部涉及类名的时候都应该跟<T>,类后面跟虚拟类型,来标识是一个类模板,构造与析构函数原名也变成了带T,但是c++允许我们简写不带T。当模板函数中传参用到类模板,如people<T>这时候不需要传参,因为这里的T只是标识是一个模板函数。
people<T>
- 当类的成员函数写在外部的时候除了要写作用域符(加<T>)还需要重新使用模板来定义虚拟类型。如下示例,当然如果将函数实体写在类的内部则啥也不需要(作用域、虚拟类型模板)。
template <typename T> people<T>::people(T a)
- 模板的虚拟类型,必须在实例化对象的时候指定具体的类型,指定方法如下,一旦指定类型后传入的参数就应该与指定类型相同,如下示例。
people<int> p(5); //定义一个对象时指定类型
多模板参数类模板
- 模板参数可以给一个虚拟类型,当然也可以定义多个示例如下
template<typename T1,typename T2> class people
- 类的成员方法外部实体也应该写成多参,如构造函数
template < typename T1, typename T2> people<T1,T2>::people(T1 a, T2 b)
- 实例化类也应该多类型
people<int,int> p(5,6); //定义一个对象时指定类型
模板友元函数
模板友元函数可以有3种写法,函数形参不带模板、友元函数内置、外置函数体。
函数形参不带模板
友元函数声明在class内,定义实现写在class外;
函数形参传入类的时候直接给出具体类型,示例如下,这种方式削弱了模板参数的使用。
template <typename T> class people
{
public:
friend void print(const people<int> &p);
};
friend void print(const people<int> &p);
友元函数内置
将友元函数写在类的内部,虽然是在类的内部,但是他不属于类的成员,好处是可以跟着类,适配各种模板参数。坏处是将友元函数在了类中,不伦不类,示例如下。
template <typename T> class people
{
private:
T age;
public:
friend void print(const people<T> &p)
{
cout<<"age= "<<p.age<<endl;
}
};
带模板外置友元
- 友元函数声明在class内,外置函数实体。
- 声明时函数名后加<T>,而定义时不能加,示例如下:
template <typename T> class people //定义类
{
public:
T age;
friend void print<T>(const people<T> &p); //函数名后必须加<T>
};
template <typename T> void print(const people<T> &p)// 函数名后不能加<T>
{
cout<<"age= "<<p.age<<endl;
}
- class和friend函数前置声明,示例如下;
template <typename T> class people; //必须前置声明
template <typename T> void print(const people<T> &p); //必须前置声明
- 调用friend函数时,可以加<实参类型>,也可以不加,如果加就不能加错。
int main(void)
{
people<int> p;
print(p); // print<int>(p);
}
模板运算符重载
模板运算符重载没有什么新的知识点,基本就是前面的运算符重载与模板类的知识结合,示例如下。
template<typename T> class people
{
private:
T age;
public:
people<T> operator+(people<T> &p); //运算符重载申明
};
template<typename T> people<T> people<T>::operator+(people<T> &p)
{
people<T> temp;
temp = age+p.age;
return temp;
}
模板友元函数运算符重载
模板友元函数实现运算符重载也没有新的知识点,同样是运算符重载、友元函数、模板类的知识点结合。前面实现模板友元函数时少写了一种方法,示例如下,在类中声明为友元函数,外部写函数实体,不同之处是在类中重新声明一个模板虚拟字符,这样就不会与类的模板名称冲突,不用对类和友元函数做前置声明,使用起来更简单。
template<typename T> class people
{
private:
T age;
public:
template<typename B> friend people<B> operator+(people<B> &a,people<B> &b);
};
template<typename T> people<T> operator+(people<T> &a, people<T> &b)
{
people<T> temp;
temp.age=a.age + b.age;
return temp;
}
模板类的继承
类模板继承类模板(单模板参数)
单模板参数,类模板继承类模板示例如下,其实模板参数有两级,子类的模板参数类型T,传给父类模板参数类型T,如标注①,两个T就像两个函数的形参变量名称相同,但是不是一个东西,完全可以写成不同。
如标注②,在子类构造函数调用父类构造函数时,需要指定people函数的所在域people<T>,如果直接写people()函数,编译器找不到,但是直接写people<T>,编译器可以找到。按构造函数与类名相同的规则,构造函数的完整名应该是people<T>,只是编译器允许我们写成people<T>::people( );
template<typename T> class people
{
public:
int x;
people(){}; //该构造函数原型是people<T>()
people(T a):x(a){}; //该构造函数原型是people<T>(T a):x(a)
};
template<typename T> class man:public people<T>①
{
public:
int y;
man(){};
man(T a,T b):y(a),people<T>::people(b)②{ };
};
类模板继承类模板(双模板参数)
“类模板继承类模板(双模板参数)”与“类模板继承类模板(单模板参数)”套路基本是相同的,不同的是关于模板参数的顺序有点绕,总而言之模板的作用域内以“虚拟类型名称(T1,T2)”进行匹配,作用域外的虚拟类型的传递以先后顺序匹配。示例如下,在标注②中以虚拟类型名匹配,与顺序无关。而②传给①时,以虚拟类型顺序匹配,与名称无关,即U1=T1,U2=T2。
template<typename T1,typename T2> class people① //标注②与本行内以位置匹配,
{
public:
T1 x1;
T2 x2;
people(){};
people(T1 a,T2 b):x1(a),x2(b){};
};
template<typename U1,typename U2> class man: public people<U1,U2>②//本行内以名称匹配
{
public:
U1 y1;
U2 y2;
man(U1 a,U2 b, U1 c,U2 d):y1(a),y2(b),people<U1,U2>(c,d) { }
};
int main(void)
{
man<int,double> p1(2, 2.2 , 5 , 5.2 );
cout << "y1= " << p1.y1 << " y2= " << p1.y2 << endl;
cout << "x1= " << p1.x1 << " x2= " << p1.x2 << endl;
return 0;
}
类模板
类模板,就是类的参数用到
template<typename T1 > class people
{
}
模板类
是一个类型确定的类,只不过类中用到了模板
class people
{
}
类模板 继承 模板类
本来子类和父类都是一个模板类,都需要实例化的时候传入实际的类型,但是子类在继承父类的时候直接给父类了一个普通类型如int,这时候父类已经是一个有具体类型的模板,在子类实例化时,就只需要对子类的类型指定,父类不需要,如下示例:
template<typename T> class people //定义一个类模板
{
public:
T age;
people (T a): age (a){ } ;
}
template<typename T> class man:public A<int>(5) //定义一个类模板,同时将父类的类型指定
{
public:
T height;
man (T h): height (h){ } ;
}
非类型模板和模板推导
非类型模板
非类型模板,示例如下,实现一个指定大小和类型的数组,在定义模板虚拟类型的同时设定一个普通变量,在实例化对象时除了传入类型,还可以传入一个数字,同时还可以为其设置默认值。
template<typename T,int lenth=10> class teble //定义一个模板类
{
T array[lenth];
};
teble<char,10> p; //使用模板类实例化对象
类型推导的隐式类型转换
- 编译器用值类型实例化函数模板,而不是用相应的引用类型,如下
int a, //定义一个int型变量
int &b=a; //b引用a
people<b>//b=int而不等于int &
- 编译器用指针类型实例化函数模板,而不是相应的数组类型,如下示例
int a[5];
people<a> //a是一个int型指针,不是数组
- 编译器去除const修饰,绝不会用const类型实例化函数模板,总是用相应的cosnt类型。
const int a = 5;
People<a> //a是int,不是cosnt int
模板和库
模板由于含有虚拟类型,所以模板库不能单独被编译,当然就更不可能做成lib静态库,所以模板库都是开源的。