c++中模板详解

文章来源:

http://blog.csdn.net/shift_wwx/article/details/78677119

 

前言:

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

这一文结合自己的想法做一个总结。

 

1、 函数模板和模板函数

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

如函数的重载,多个函数除了数据类型不同,而函数算法 相同时,可以用函数模板。

定义形式:

template

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

    函数体;

}

  • template是模板定义的关键字,不做说明
  • 模板类型用class,这里和typename是一样的,具体看typename 和class在template中的区别
  • 函数返回类型可以是模板类型形参名
  • 函数形参表中形参形式分为:引用型参数和非引用型参数。引用型参数直接修改实参本身。

在调用函数模板时,编译系统会根据实参的类型生成一个对应的函数,这就是模板函数。

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

 

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

例如,

templateswap(T a, T b) {
    ...
	...
}

在调用swap的时候,两个实参的类型必须都是T,swap(1, 2); 或者swap(1.2, 2.1);都是可以的,但是不能是swap(1, 2.1); 一个是int类型,一个是float类型。

 

2、类模板

定义形式:

template 
class className {
   类体;
};

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

template <类型形参表>
返回类型 className<类型形参名>::functionName(函数形参表) {
    函数体;
}
  • template后面的类型形参表和类模板定义的时候一样
  • className后面的类型形参名就是类模板定义中的类型形参名,不加class或typename等关键字
  • 函数的形参表就是形参,可能会用到类型形参

 

比如有两个模板形参T1,T2的类A中含有一个void h()函数,则定义该函数的语法为:

template void A::h(){}

 

类模板实例:

className<类型实参表> object;

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

A> a;

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

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

注意:

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

 

3、模板形参

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

类型形参

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

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

 

非类型形参

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

非类型模板形参的形参和实参间所允许的转换

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

 

模板形参

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

形式如下:

template class ParameterName>

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

 

4、默认模板参数

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

例如:

template
class Test4 {
public:
    Test4();
    Test4(T1, T2);
    ~Test4();
    void test(T1, T2);
private:
    T1 value1;
    T2 value2;
};

指定第二个形参为int型,如果默认情况下第二个参数为int,实例obj的时候,可以这样:

    Test4 obj;
    obj.test(5.0, 6);

编译器会自动将匹配到正确的模板函数,如果与默认参数的类型不匹配的时候会编译报错。例如,默认参数改为int*或者char*。

注意:

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

 

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

声明
声明就是让编译器知道有这样的一个函数或者类,一个模板形式为
template void test(T);
template class A;

这就是模板的声明,后面没有函数体或者类体,注意A后面的分号。

定义

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

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

例如,普通类A可以定义为:

A::A(){}
A::~A(){}
void A::test(){}

换成类模板应该改为

template
A::A(){}

template
A::~A(){}

template
void A::test(T){}

实例化

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

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

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

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

 

6、实参推演

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

template void swap(T x, T y){}

在调用swap(3, 2); 的时候会根据实参推演出swap( int, int);并且建立实例。

当然,这个实例建立好后再次调用swap(2, 3); 是不会在建立实例,会使用已经有的。

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

templatevoid h(T x){}

void main() {
    int a = 2;
    short b = 3;
    h(a);
    h(b);
}

最开始使用h(a); 会建立一个实例,类型为int。在使用h(b); 的时候会再次建立一个实例,类型为short。并不会像普通函数那样存在类型转换。

 

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

(1)数组到指针的转换

template 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 void h(const T* a){},int b=3; h(&b);虽然实参&b与形参const T*不完全匹配,但因为允许限制修饰符的转换,结果就把&b转换成const int *。而类形型参T被转换成int。如果模板形参是非const类型,则无论实参是const类型还是非const类型调用都不会产生新的实例。

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

例如,

templateclass A{};
template class B:public A{};
template void h(A& m){}

在main函数中有B n; h(n);函数调用的子类对象n与函数的形参A不完全匹配,但允许到一个基类的转换。

在这里转换的顺序为,首先把子类对象n转换为基类对象A,然后再用A去匹配函数的形参A&,所以最后T2被转换为int,也就是说函数体中的T将被替换为int。

 

7、显示实例化

隐式实例化

例如有模板函数

template void h(T a){}

h(2)这时h函数的调用就是隐式实例化,既参数T的类型是隐式确定的。

函数模板显示实例化

语法是:

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

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

template  void h (int a);

这样就创建了一个h函数的int 实例。

 

再如有模板函数

template T h( T a){}

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

注意:

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

 

8、显示模板实参

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

格式:

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

例如,有模板函数

template void h(T a, T b){}

则h(2, 3.2)就把模板形参T显示实例化为double类型。

 

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

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

 

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

例如有模板函数

template T1 h(T2 a, T3 b){}

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

 

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

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

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

例如,

template void h(T b){}

而调用h(3)将出错,因为这个调用无法为非类型形参推演出正确的参数。这时正确调用这个函数模板的方法为h(4),首先把函数模板的类型形参T推演为int 型,然后把函数模板的非类型形参int a用数值3来推演,把变量a设置为3,然后再把4传递给函数的形参b,把b设置为4。注意,因为int a是非类型形参,所以调用非类型形参的实参应是编译时常量表达式,不然就会出错。

 

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

例如,

template T1 h(T2 a, T3 b){}

在显示实例化时h(3, 3.4)省略了最后两个模板实参T2和T3,T2和T3由调用时的实参3和3.4隐式确定为int 型和double型,而T1被显示确定为int 型。h, , double><2,3.4>是错误的,只能省略尾部的实参。

显示模板实参最好用在存在二义性或模板实参推演不能进行的情况下。

 

9、模板特例化

templatevoid h(T a){}

这个函数体中的功能使用所有类型,但是如果int型比较特殊,不需要这里的函数体。这样就需要对模板进行特殊化。

函数模板特例化格式:

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

  • 显示特化以template<>开头,表明要显示特化一个模板
  • 在函数名后<>用尖括号括住要特化的类型版本。

 

对于上面的函数模板,其int 类型的特化版本为template<> void h(int a){}

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

 

如果可以从实参中推演出模板的形参,则可以省略掉显示模板实参的部分。

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


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

template T1 h(T2 a,T3 b){}

有几种特化情况:

情况一:

template<> int h(int a, in b){}

该情况下把T1,T2,T3的类型推演为int 型。在主函数中的调用方式应为h(2,3)。

 

情况二:

template<> int h(int a, int b){}

这里把T2,T3推演为int 型,而T1为int 型,但在调用时必须用显示模板实参调用,且在<>尖括号内必须指定为int 型,不然就会调用到通用函数模板,如h(2,3)就会调用函数模板的特化版本,而h(2,3)调用会出错。h(2,3)调用则会调用到通用的函数模板版本。

 

下面几种情况的特化版本是错误的,例如,

template<> T1 h(int a,int b){}

这种情况下T1会成为不能识别的名字,因而出现错误。

template<> int h(int a,int b){}

在这种情况下返回类型为int 型,把T1确定为int 而尖括号内又把T1确定为double型,这样就出现了冲突。

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

 

类模板特例化格式:

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

例如,

template class A{};

特例化为:

template<> class A{};

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

例如

template class A{public: void h();};

类A特化为

template<>  class A{public: void h();};

在类外定义特化的类的成员函数h的方法为:

void A::h(){}

在外部定义类特化的成员时应省略掉template<>。

 

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

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

 

类模板的部分特例化

比如有类模板

template class A{};

则部分特化的格式为

template class A{};

将模板形参T2特化为int 型,T1保持不变。

 

部分特化以template开始,在<>中的模板形参是不用特化的模板形参,在类名A后面跟上要特化的类型。

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

template class A{};

部分特化的另一用法是:

template class A{};

将模板形参T2也特化为模板形参T1的类型。

 

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

例如有部分特化类

template class A{public: void h();};

则在类外定义的形式为:

template void A::h(){}

注意当在类外面定义类的成员时template 后面的模板形参应与要定义的类的模板形参一样,这里就与部分特化的类A的一样template

 

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

 

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

template void h(T a){}

在h(2)之前没有声明该模板的int 型特化版本,而是在调用该模板后定义该模板的int 型特化版本,这时程序不会调用该模板的特化版本,而是调用该模板产生一个新的实例。这里就有一个问题,到底是调用由h(2)产生的实例版本呢还是调用程序中的特化版本。
(3)因为模板的声明或定义不能在局部范围或函数内进行。所以特化类模板或函数模板都应在全局范围内进行。
(4)在特化版本中模板的类型形参是不可见的。例如,

template<> void h(int a,int b){T1 a;}

就会出现错误,在这里模板的类型形参T1在函数模板的特化版本中是不可见的,所以在这里T1是未知的标识符,是错误的。

 

10、模板与继承

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

继承方式一:

templateclass B:public A
{类体};

继承自A某一实例,这里是A的int型实例

 

继承方式二:

templateclass B:public A
{类体};

在实例化B的时候会用同样的类型实例化基类A

 

继承方式三:

class B:public A
{类体};

类B不是模板类

 

 

 

 

 

 

你可能感兴趣的:(C++)