c++中模板详解

前言:c++

在C++ template中已经详细的经过实践说明了c++ template的用法,也在typename 和class在template中的区别中解释了template中typename和class的区别。算法

这一文结合本身的想法作一个总结。数组

一、 函数模板和模板函数函数

函数模板是一个抽象画的函数,区别于函数的重载。指针

如函数的重载,多个函数除了数据类型不一样,而函数算法 相同时,能够用函数模板。对象

定义形式:继承

template<class 类型形参名, class 类型形参名>编译器

返回类型 函数名(函数形参表) {string

    函数体;io

}

template是模板定义的关键字,不作说明
模板类型用class,这里和typename是同样的,具体看typename 和class在template中的区别
函数返回类型能够是模板类型形参名
函数形参表中形参形式分为:引用型参数和非引用型参数。引用型参数直接修改实参自己。
在调用函数模板时,编译系统会根据实参的类型生成一个对应的函数,这就是模板函数。

模板函数由编译系统在发现具体的函数调用时生成相对应的程序代码,是实际的函数定义。因此,模板函数是函数模板的实例化。

在函数调用的时候,实参的参数类型必须和模板中的数据参数类型彻底一致,才能正确的实例化,才会有正确的模板函数。

例如,

template<class T>swap(T a, T b) {
    ...
    ...
}
在调用swap的时候,两个实参的类型必须都是T,swap(1, 2); 或者swap(1.2, 2.1);都是能够的,可是不能是swap(1, 2.1); 一个是int类型,一个是float类型。

二、类模板

定义形式:

template <class 类型形参名, class 类型形参名>
class className {
   类体;
};

对于类模板的类型形参后面说明,来看一下类模板中的成员函数的实现形式:

template <类型形参表>
返回类型 className<类型形参名>::functionName(函数形参表) {
    函数体;
}
template后面的类型形参表和类模板定义的时候同样
className后面的类型形参名就是类模板定义中的类型形参名,不加class或typename等关键字
函数的形参表就是形参,可能会用到类型形参
好比有两个模板形参T1,T2的类A中含有一个void h()函数,则定义该函数的语法为:

template<class T1, class T2> void A<T1,T2>::h(){}


类模板实例:

className<类型实参表> object;
这里className后面的是类型的实参表了,一样是上面定义的模板类A,形参名为T1和T2,可是在实例的时候应该写为:

A<int, int> a;

这样对象a中的类型都是int型替换。

对于类模板,模板的形参必须在类名后用尖括号明确指定,这跟函数模板不同的地方。

注意:

模板的声明或定义只能在全局、命名空间或者类范围内进行。不能放到函数或者局部范围内进行。

三、模板形参

模板的形参分三种:类型形参、非类型形参、模板形参。

类型形参

在这以前讲到的模板形参都是类型形参,用关键字class或者typename声明。

实例化都是根据实参的真正类型。

非类型形参

非类型形参又称内置类型形参。例如templage<class T, int a>class A{}; 其中int a就是非类型形参。
对于非类型形参,在模板内部是以常量形式存在,因此又称为内置类型形参。
非类型形参只能是整型、指针和引用。double、string等是不容许的,但double *、double &和对象的引用或指针是能够的。
调用非类型形参的实参必须是常量表达式,也就是说在编译的时候必须计算出结果。
注意:任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用做非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用做非类型模板形参的实参。
全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,能够用做非类型模板形参的实参。
sizeof表达式的结果是一个常量表达式,也能用做非类型模板形参的实参。
当模板的形参是整型时调用该模板时的实参必须是整型的,且在编译期间是常量,好比template <class T, int a> class A{};若是有int b,这时A<int, b> m;将出错,由于b不是常量,若是const int b,这时A<int, b> m;就是正确的,由于这时b是常量。
非类型形参通常不该用于函数模板中,好比有函数模板template<class T, int a> void h(T b){},若使用h(2)调用会出现没法为非类型形参a推演出参数的错误,对这种模板函数能够用显示模板实参来解决,如用h<int, 3>(2)这样就把非类型形参a设置为整数3。显示模板实参在后面介绍。
非类型模板形参的形参和实参间所容许的转换

容许从数组到指针,从函数到指针的转换。如:template <int *a> class A{}; int b[1]; A<b> m;即数组到指针的转换
const修饰符的转换。如:template<const int *a> class A{}; int b; A<&b> m;   即从int *到const int *的转换。
提高转换。如:template<int a> class A{}; const short b=2; A<b> m; 即从short到int 的提高转换
整值转换。如:template<unsigned int a> class A{};   A<3> m; 即从int 到unsigned int的转换。
常规转换。


模板形参

模板形参就是模板的参数是个类模板模板。

形式以下:

template<class T, template <class U> class ParameterName>

上面模板形参中标记的class不能用typename替换
模板形参并不用于函数模板中


四、默认模板参数

指定模板中形参类型为默认值,这样在实例的时候可使用默认参数。

例如:

template<class T1, class T2 = int>
class Test4 {
public:
    Test4();
    Test4(T1, T2);
    ~Test4();
    void test(T1, T2);
private:
    T1 value1;
    T2 value2;
};
指定第二个形参为int型,若是默认状况下第二个参数为int,实例obj的时候,能够这样:
    Test4<float> obj;
    obj.test(5.0, 6);
编译器会自动将匹配到正确的模板函数,若是与默认参数的类型不匹配的时候会编译报错。例如,默认参数改成int*或者char*。
注意:

默认模板参数不适用于模板函数,包括全局函数和类成员函数。
若是模板形参为默认参数,那后面的形参都必须设置默认值。
类外定义成员函数的时候,默认参数应该省略

五、模板声明、定义、实例化的概念

声明
声明就是让编译器知道有这样的一个函数或者类,一个模板形式为
template<class T> void test(T);
template<class T> class A;
这就是模板的声明,后面没有函数体或者类体,注意A后面的分号。


定义

定义跟普通的函数定义、类定义是同样的。

注意类模板的定义方式,其实除了加上了template<class T>前缀,和className<T>指定域,和普通类并无区别。

例如,普通类A能够定义为:


A::A(){}
A::~A(){}
void A::test(){}
换成类模板应该改成
template<class T>
A<T>::A(){}
 
template<class T>
A<T>::~A(){}
 
template<class T>
void A<T>::test(T){}


实例化

实例化是在模板调用的时候,例如A<T> obj;

若是建立了这样的实例,在下次再次条用一样的模板实例的时候,是不会建立新的实例。例如,A<int> obj;就建立了一个int型的实例,下次在建立另外一个A<int> obj2;的时候是不会建立新实例。

对于指针或者引用,以后在真正指向相关的对象的时候才会实例化。例如A *m; 或 A &n;并不会实例化,可是m = new A():就会实例化。

下面会在实参推演的过程当中,说明实例化的其余注意事项。

六、实参推演

模板的实例化是在模板调用的时候,例如,

template<class T> void swap(T x, T y){}
在调用swap(3, 2); 的时候会根据实参推演出swap( int, int);而且创建实例。
固然,这个实例创建好后再次调用swap(2, 3); 是不会在创建实例,会使用已经有的。

对于模板,实例化会创建实例,可是并不会出现类型转化。例如,


template<class T>void h(T x){}
 
void main() {
    int a = 2;
    short b = 3;
    h(a);
    h(b);
}
最开始使用h(a); 会创建一个实例,类型为int。在使用h(b); 的时候会再次创建一个实例,类型为short。并不会像普通函数那样存在类型转换。

编译器容许下面实参到模板形参的转换:

(1)数组到指针的转换

template<class T> void h(T *x){}
 
int a[] = {1, 2, 3};
h(a);
能够看到模板形参为指针类型,实参为数组类型。编译器容许数组到指针的转换,这个时候会实例化一个h(int *);的实例,T会被转换为int,函数体中的T会被int替换。换言之,若是已经存在了一个h(int *)的实例,这个时候的数组调用时不会产生新的实例,会直接使用h(int *);
(2)限制修饰符转换
即把const或volatile限定符加到指针上。好比template<class T> void h(const T* a){},int b=3; h(&b);虽然实参&b与形参const T*不彻底匹配,但由于容许限制修饰符的转换,结果就把&b转换成const int *。而类形型参T被转换成int。若是模板形参是非const类型,则不管实参是const类型仍是非const类型调用都不会产生新的实例。

(3)到一个基类的转换(基类为一个模板类)

例如,


template<class T1>class A{};
template<class T1> class B:public A<T1>{};
template<class T2> void h(A<T2>& m){}
在main函数中有B<int> n; h(n);函数调用的子类对象n与函数的形参A<T2>不彻底匹配,但容许到一个基类的转换。
在这里转换的顺序为,首先把子类对象n转换为基类对象A<int>,而后再用A<int>去匹配函数的形参A<T2>&,因此最后T2被转换为int,也就是说函数体中的T将被替换为int。

七、显示实例化

隐式实例化

例若有模板函数


template<class T> void h(T a){}
h(2)这时h函数的调用就是隐式实例化,既参数T的类型是隐式肯定的。


函数模板显示实例化

语法是:

    template  函数反回类型 函数名<实例化的类型> (函数形参表); 

注意这是声明语句,要以分号结束。例如,


template  void h<int> (int a);
这样就建立了一个h函数的int 实例。
再若有模板函数


template<class T> T h( T a){}
注意这里h函数的反回类型为T,显示实例化的方法为template int h<int>(int a); 把h模板函数实例化为int 型。


注意:

对于给定的函数模板实例,显示实例化声明在一个文件中只能出现一次。
在显示实例化声明所在的文件中,函数模板的定义必须给出,若是定义不可见,就会发生错误。
不能在局部范围类显示实例化模板,实例化模板应放在全局范围内,即不能在main函数等局部范围中实例化模板。由于模板的声明或定义不能在局部范围或函数内进行。


八、显示模板实参
适用于函数模板,即在调用函数时显示指定要调用的时参的类型。


格式:

在调用模板函数的时候在函数名后用<>尖括号括住要显示表示的类型

例如,有模板函数


template<class T> void h(T a, T b){}
则h<double>(2, 3.2)就把模板形参T显示实例化为double类型。


显示模板实参用于同一个模板形参的类型不一致的状况。

对于上面的模板,h(2, 3.2)的调用会出错,由于两个实参类型不一致,第一个为int 型,第二个为double型。而用h<double>(2, 3.2)就是正确的,虽然两个模板形参的类型不一致但这里把模板形参显示实例化为double类型,这样的话就容许进行标准的隐式类型转换,即这里把第一个int 参数转换为double类型的参数。

显示模板实参用于函数模板的返回类型中。

例若有模板函数


template<class T1, class T2, class T3> T1 h(T2 a, T3 b){}
则语句int a=h(2,3)或h(2,4)就会出现模板形参T1没法推导的状况。而语句int h(2,3)也会出错。用显示模板实参就参轻松解决这个问题,好比h<int, int, int>(2,3)即把模板形参T1实例化为int 型,T2和T3也实例化为int 型。

显示模板实参应用于模板函数的参数中没有出现模板形参的状况。

例如template<class  T>void h(){}若是在main函数中直接调用h函数如h()就会出现没法推演类型形参T的类型的错误,这时用显示模板实参就不会出现这种错误,调用方法为h<int>(),把h函数的模板形参实例化为int 型,从而避免这种错误。


显示模板实参用于函数模板的非类型形参。

例如,


template<class T,int a> void h(T b){}
而调用h(3)将出错,由于这个调用没法为非类型形参推演出正确的参数。这时正确调用这个函数模板的方法为h<int, 3>(4),首先把函数模板的类型形参T推演为int 型,而后把函数模板的非类型形参int a用数值3来推演,把变量a设置为3,而后再把4传递给函数的形参b,把b设置为4。注意,由于int a是非类型形参,因此调用非类型形参的实参应是编译时常量表达式,否则就会出错。

在使用显示模板实参时,咱们只能省略掉尾部的实参。

例如,


template<class T1, class T2, class T3> T1 h(T2 a, T3 b){}
在显示实例化时h<int>(3, 3.4)省略了最后两个模板实参T2和T3,T2和T3由调用时的实参3和3.4隐式肯定为int 型和double型,而T1被显示肯定为int 型。h<int, , double><2,3.4>是错误的,只能省略尾部的实参。
 
显示模板实参最好用在存在二义性或模板实参推演不能进行的状况下。

九、模板特例化


template<class T>void h(T a){}
这个函数体中的功能使用全部类型,可是若是int型比较特殊,不须要这里的函数体。这样就须要对模板进行特殊化。

函数模板特例化格式:

    template<>  返回类型 函数名<要特化的类型>(参数列表) {函数体}

显示特化以template<>开头,代表要显示特化一个模板
在函数名后<>用尖括号括住要特化的类型版本。
对于上面的函数模板,其int 类型的特化版本为template<> void h<int>(int a){}

当出现int 类型的调用时就会调用这个特化版本,而不会调用通用的模板,好比h(2),就会调用int 类型的特化版本。

若是能够从实参中推演出模板的形参,则能够省略掉显示模板实参的部分。

例如:template<> void h(int a){}。注意函数h后面没有<>符号,即显示模板实参部分。


对于反回类型为模板形参时,调用该函数的特化版本必需要用显示模板实参调用,若是不这样的话就会出现其中一个形参没法推演的状况。例如,


template<class T1,class T2,class T3> T1 h(T2 a,T3 b){}
有几种特化状况:
状况一:
template<> int h<int,int>(int a, in b){}
该状况下把T1,T2,T3的类型推演为int 型。在主函数中的调用方式应为h<int>(2,3)。
状况二:
template<> int h(int a, int b){}
这里把T2,T3推演为int 型,而T1为int 型,但在调用时必须用显示模板实参调用,且在<>尖括号内必须指定为int 型,否则就会调用到通用函数模板,如h<int>(2,3)就会调用函数模板的特化版本,而h(2,3)调用会出错。h<double>(2,3)调用则会调用到通用的函数模板版本。
下面几种状况的特化版本是错误的,例如,
template<> T1 h(int a,int b){}
这种状况下T1会成为不能识别的名字,于是出现错误。


template<> int h<double>(int a,int b){}
在这种状况下返回类型为int 型,把T1肯定为int 而尖括号内又把T1肯定为double型,这样就出现了冲突。

具备相同名字和相同数量返回类型的非模板函数(即普通函数),也是函数模板特化的一种状况,这种状况将在后面参数匹配问题时讲解。

类模板特例化格式:

    template<>  class 类名<要特化的类型> {类体};

例如,


template<class T1,class T2> class A{};
特例化为:
template<> class A<int, int>{};


在类特化的外部定义成员的方法
例如

template<class T> class A{public: void h();};
类A特化为
template<>  class A<int>{public: void h();};
在类外定义特化的类的成员函数h的方法为:
void A<int>::h(){}
在外部定义类特化的成员时应省略掉template<>。

类的特化版本应与类模板版本有相同的成员定义,若是不相同的话那么当类特化的对象访问到类模板的成员时就会出错。

由于当调用类的特化版本建立实例时建立的是特化版本的实例,不会建立类模板的实例,特化版本若是和类的模板版本的成员不同就有可能出现这种错误。好比:模板类A中有成员函数h()和f(),而特化的类A中没有定义成员函数f(),这时若是有一个特化的类的对象访问到模板类中的函数f()时就会出错,由于在特化类的实例中找不到这个成员。


类模板的部分特化

好比有类模板

template<class T1, class T2> class A{};
则部分特化的格式为
template<class T1> class A<T1, int>{};
将模板形参T2特化为int 型,T1保持不变。
部分特化以template开始,在<>中的模板形参是不用特化的模板形参,在类名A后面跟上要特化的类型。

若是要特化第一个模板形参T1,则格式为

template<class T2> class A<int, T2>{};
部分特化的另外一用法是:
template<class T1> class A<T1,T1>{};
将模板形参T2也特化为模板形参T1的类型。

在类部分特化的外面定义类成员的方法

例若有部分特化类

template<class T1> class A<T1,int>{public: void h();};
则在类外定义的形式为:
template<class T1> void A<T1,int>::h(){}
注意当在类外面定义类的成员时template 后面的模板形参应与要定义的类的模板形参同样,这里就与部分特化的类A的同样template<class T1>。

其余说明:
(1)能够对模板的特化版本只进行声明,而不定义。好比template<> void h<int>(int a);注意,声明时后面有个分号。
(2)在调用模板实例以前必需要先对特化的模板进行声明或定义。

一个程序不容许同一模板实参集的同一模板既有显示特化又有实例化。例如,

template<class T> void h(T a){}
在h(2)以前没有声明该模板的int 型特化版本,而是在调用该模板后定义该模板的int 型特化版本,这时程序不会调用该模板的特化版本,而是调用该模板产生一个新的实例。这里就有一个问题,究竟是调用由h(2)产生的实例版本呢仍是调用程序中的特化版本。
(3)由于模板的声明或定义不能在局部范围或函数内进行。因此特化类模板或函数模板都应在全局范围内进行。
(4)在特化版本中模板的类型形参是不可见的。例如,

template<> void h<int,int>(int a,int b){T1 a;}
就会出现错误,在这里模板的类型形参T1在函数模板的特化版本中是不可见的,因此在这里T1是未知的标识符,是错误的。


十、模板与继承

子类并不会从通用的模板基类继承而来,只能从模板基类的某一实例继承而来。

继承方式一:


template<class T1>class B:public A<int>
{类体};
继承自A某一实例,这里是A的int型实例


继承方式二:


template<class T1>class B:public A<T1>
{类体};
在实例化B的时候会用一样的类型实例化基类A


继承方式三:

class B:public A<int> {类体}; 类B不是模板类

相关文章
相关标签/搜索