模板并不是一个实实在在的类或函数,仅仅是一个类或函数的描述。
模板使类或函数可在编译时定义所需处理和返回的数据类型,有利于代码的重用。
函数返回值类型可以返回除了函数和数组以及类之外的任意类型。
1、函数模板
1.1、定义一个函数模板
函数模板并不是一个可以直接使用的函数,它时可以产生多个函数的模板,即一个函数可以适应不同数据类型。定义如下:
1 | template<typename/class 形参名,typename/class 形参名......> |
其中,template是声明模板的关键字,typename和class是定义形参的关键字,这里typenam和class没有区别。<>的参数称为模板形参,模板形参和函数形参很像,但模板形参不能为空。
template下面就是定义的一个函数模板,它与普通的函数定义方式相同,只是参数列表中的数据类型要使用<>中的形参名。
1.2、函数模板的实例化
函数模板并不是一个函数,它只相当于一个模子,定义一次即可使用不同类型的参数来调用该函数,即可以提高代码的复用性。但使用函数模板并不会减少最终可执行程序的大小,因为在调用函数模板时,编译器会根据调用时的参数类型进行相应的实例化。而实例化就说用类型参数去替换模板中的模板参数,生成一个具体类型的真正函数。
1.2.1、隐式实例化
隐式实例化时根据函数调用时传入的数据类型确定模板形参T的类型,模板形参的类型是隐式确定的。例如:
1 |
|
如上所示,当第一次调用add()函数模板时,传入的是int型数据add(1,2),此时编译器根据传入的实参推演出模板形参类型是int,就会将函数模板实例化出一个int类型的函数,如下图:
1 | int add(int t1,int t2) |
编译器生成具体类型函数的这一过程就称为实例化,生成的函数称为模板函数。
1.2.2、显式实例化
隐式实例化不能为同一个模板形参指定两种不同的类型,比如add(1,1,2),这样调用,两张形参类型不一致,编译器便会报错。此时可以使用显式实例化来解决。
显式实例化就是显式地指定函数模板中的数据类型,如:
1 | template 函数返回值类型 函数名<实例化的类型>(参数列表) |
1 |
|
如上所示,程序中将add()函数模板显式声明,指定模板形参为int类型,在调用int类型模板函数时,传入了一个字符‘B’,则编译器会将字符类型的‘B’转换为int类型,任何再代回函数体执行。实际上就是一个隐式的数据类型转换。
注意:对于给定的的函数模板实例,显式实例化声明在一个文件中只能出现一次,并且在这个文件中必须给出函数模板的定义,如果定义不可见就会发生错误。
现在C++编译器也在不断完善,像模板实例化的显式声明有时可以省略,只在调用时用<>显式指定要实例化的类型也可以,如上式中的add(1.2,2.4)函数调用如果改为add
1.2.3、显式具体化
函数模板的显式具体化是为了实现一种不同于函数模板定义的实现,具体如下:
1 | template<>函数返回值类型 函数名<实例化类型>(参数列表); |
它的定义格式与显式实例化很相似,但是显式实例化只需要显式声明模板参数的类型,不需要重新定义函数的实现,而显式具体化则需要重新定义函数模板的实现以达到自己想要的处理结果。例如:
1 | template<typename T> |
现在要调换两个学生的id编号,但是又不能交换学生的其它信息,则此时就需要用显式具体化来解决这个问题。
模板的显式具体化有点类似于类中的函数重写,但函数重写是在类层次结构中不同的作用域,而模板显式具体化是在同一作用域。
如果函数有多个原型,则编译器在选择函数调用时,有以下顺序:
直接定义>显式具体化>模板定义,即:
1 | void swap(Student &,Student &);//直接定义 |
注意:要先有模板定义,才有显式具体化。
1.2.4、函数模板的重载
函数模板实例化不同类型参数的模板函数就是重载函数,除此之外,函数模板本身也可以被重载,即相同函数模板名可以具有不同的函数模板定义,当进行函数调用时,编译器根据实参的类型和个数来决定调用哪个函数模板来实例化。
例如:
1 |
|
如上程序所示,对于非模板函数和同名的函数模板,如果其它条件都相同,那么在调用的时候重载解析过程中会调用非模板函数,而不会调用函数模板来实例化一个函数模板,即以上的第一个函数调用。
然而如果函数模板能够更好地实例化出一个匹配的函数,则调用时将选择函数模板,如以上的第三个函数调用,利用函数模板实例化出一个带有char类型参数的模板函数,则不会调用非模板函数max(int,int)。
注意:如果有不同类型参数,则只允许使用非模板函数,因为模板是不允许自动类型转换的(显式实例化也可以),但是普通函数可以进行自动类型转换,所以最后一个函数调用是非模板函数调用,将3.2转换成了int类型再与6比较。
1.2.5、使用函数模板需要注意的问题
1、函数模板中的每一个类型参数在函数参数表中必须至少使用一次,即以下例子中,T2没有使用一次:
1 | template<typename T1,typename T2> |
2、在全局域中声明的与模板参数同名的对象、函数、或类型,在函数模板中将被隐藏。例如以下例子中,在函数体中访问num是访问的T类型的num,而不是全局变量num。
1 | int num; |
3、函数模板中定义声明的对象或类型不能与模板参数同名。例如:
1 | template<typename T> |
4、模板参数名在统一模板参数表中只能使用一次,但可在多个函数模板声明或定义之间重复使用。
例如:
1 | template<typename T,typename T>//错误,在同一个模板中重复定义模板参数 |
5、模板的定义和多处声明所使用的模板参数名不一定要相同。
6、函数模板如果有多个模板参数,则每个模板类型前都必须使用关键字typename或class修饰。
2、类模板
2.1、类模板的定义和实例化
函数可以定义模板,对于类来说,也可以定义一个类模板,类模板是针对成员数据类型不同的类的抽象,它不是代表一个具体的实际的类,而是一个类型的类,一个类模板可以生成多种具体的类,定义类模板的格式如下:
1 | template<typename 形参名,typename 形参名...> |
类模板中的关键字含义与函数模板相同,并且类模板的模板形参也不能为空,一旦声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即可以在类中使用数据类型的地方都可以使用模板形参来声明。
由于类模板包含类型参数,因此也称为参数化类,如果说类是对象的抽象,对象是类的实例,那么类模板是类的抽象,类是类模板的实例。
定义了类模板后就要使用类模板创建对象以及实现类中的成员函数,这个过程就是类模板实例化的过程,实例化出的具体类称为模板类。
类模板创建对象格式:
1 | 类名<数据类型> 对象名; |
当类模板有两个模板形参时,类型之间要用逗号隔开。即:
1 | template<typename T1,typename T2> |
使用类模板时,必须为模板形参显式指定实参,不存在实参推演过程,即类似于函数模板的显式实例化。(必须要在<>中指定int类型)
注意:类模板在实例化时,带有模板形参的成员函数并不会跟着实例化,这些成员函数只有在被调用时才会被实例化。
2.1.2、在类模板外部定义成员函数
类模板的成员函数可以在类模板中定义(inline函数),也可以在类模板外定义(此时成员函数定义前面必须加上template及模板参数),即:
1 | template<模板参数名> |
template是类模板的声明,在实现成员函数时,也要加上类作用域,并且在类名后要用<>知名类的模板形参。例如:
1 | template<typename T1,typename T2> |
注意:当在类外定义类的成员函数时,template后面的模板形参应该要与定义的类模板形参一致。
类模板成员函数本身也是一个模板,类模板被实例化时它并不自动被实例化,只有当它被调用或取地址时,才被实例化。
2.1.3、类模板与友元函数
C++中的类可以声明友元函数和友元类,同样类模板也可以声明友元函数和友元类,在类模板中声明的友元函数有三类:非模板友元函数、约束模板友元函数、非约束模板友元函数
2.1.3.1、非模板友元函数
非模板友元就是在类模板中声明普通的友元函数和友元类,并且不是模板,例如:
1 | template<typename T> |
在类模板A中声明了一个普通的友元函数func(),则func()函数是类模板所有实例的友元函数,它可以访问全局对象,也可以使用全局指针访问非全局对象,可以创建自己的对象,可以访问独立于对象的模板的静态数据成员。
当然,也可以为类模板的友元函数提供模板类参数,例如:
1 | template<typename t> |
上例中,show()函数并不是模板函数,而只是使用一个模板作为参数,这就要求在这种情况下使用友元函数就必须显式具体化,即指明友元函数要引用的参数的类型。例如:
1 | void show(const A<int>& a); |
也就是说模板形参为int类型的show()函数是A
例如:
1 |
|
即注意:在调用有模板形参的友元函数时,要对友元函数进行显式具体化,它们是各自相同类型的对象的友元函数。
2.1.3.2、约束模板友元函数
约束模板友元函数本身就是一个模板,但它的实例化类型取决于类被实例化时的类型(即被约束),每个类的实例化都会产自一个与之匹配的具体化的友元函数。
以以上式子为例,修改为模板友元函数:
1、在类定义的前面声明函数模板
1 | template<typename T> |
2、在类模板中将函数模板声明为类的友元函数。
1 | template<typename U> |
上述友元函数的声明中,函数名后的<>指出函数模板要实例化的类型,它是由类模板的参数类型决定的。例如,如果定义一个类模板对象,A
1 | class A |
类中友元函数会根据类的实例化类型实例化出与之匹配的具体函数,但是要注意的是,类模板的实例化不会实例化一个友元函数,只是声明友元而不进行实例化,只有在函数调用时,函数才会实例化。
上述友元函数声明中,show()函数有类的引用作为参数,可以从函数参数推断出模板类型参数,所以其函数名后的<>可以为空。
3、为友元函数模板提供定义
为函数模板提供定义,必须在类内声明,类外定义,例如:
1 | template<typename T> |
1 |
|
以上例子中,将func()函数和show()函数定义成了模板并声明了类的友元,在定义函数模板时是在类外定义的,当调用函数时,func()函数后带有<>说明函数的实例化类型,而show()是直接调用的。
2.1.3.3、非约束模板友元函数
通过在类内声明友元函数模板,可以创建非约束友元函数,即函数模板是每个类实例的友元,函数模板形参不会受到模板形参的影响,如下面定义:
1 | template<typename T> |
对于非约束友元,友元模板的参数与类模板的类型参数是不同的,在类内声明函数模板,在类外定义。
1 | template<typename U,typename U> |
函数模板的形参与类模板的形参类型不相关,因此它可以接受任何类型的参数。
3、模板声明或定义的作用域
模板的声明或定义只能在全局、命名空间或类范围内进行,不能在局部范围、函数内进行,比如不能在main()函数中声明或定义一个模板。声明和定义一个模板还有以下注意点:
1、如果在全局域中声明了与模板参数同名的变量,则该变量被隐藏。
2、模板参数名不能被当作类模板定义中类成员的名字。
3、同一个模板参数名在模板参数表中只出现一次。
4、在不同的类模板声明或定义中,模板参数名可以被重复使用。
4、派生与模板
4.1、模板的参数
模板的形参有三种类型:类型参数、非类型参数、模板形参。
4.1.1、类型参数
以typename或者class关键字标记的模板参数就称为类型模板参数,类型模板参数是我们使用模板的主要目的。例如:
1 | template<typename T> |
其中T就是一个类型形参,形参名字自定,表示是一个未知类型,模板类型形参可以作为类型说明符用在模板中的任何地方,与内置类型说明符或类型说明符的使用方式完全相同。
我们可以为模板定义多个类型模板参数,也可以为类型模板参数指定默认值,例如:
1 | template<typename T,typename U = int> |
在上述代码中,把U默认设置成为int类型,类模板类型形参和函数的默认参数意义,如果有多个形参,则第一个形参设定了默认值之后,之后的所有模板形参都要设定默认值。
注意:可以为类模板设置默认值,但不能为函数模板设置默认值。(函数模板需要当调用时才可以实例化,或者显式具体化和显式实例化才行)
4.1.2、非类型参数
模板的非类型参数也就是内置类型形参,例如:
1 | template<typename T,int a> |
其中a就是非类型的模板形参。非类型模板形参相当于为函数模板或者类模板预定义一些常量,在生成模板实例时,也要求必须以常量,即用编译器已知的值为非类型模板参数赋值。
非类型模板形参只可以是整型int、枚举enum、指针*和引用类型&,例如double不可以是非类型形参,但double*、double&这样的指针或对象引用时正确的。
相对于常量,非类型模板参数的灵活之处在于:模板中声明的常量,在模板的所有实例中都具有相同的值,而非类型模板形参则对于在不同的模板实例中拥有不同的值来满足不同的需求。
1 | template<typename T> |
例如要多个大小不一的数组时,将数组大小定义为常量就无法满足了。
1 |
|
注意:
1、调用非类型模板形参的实参必须是一个常量表达式,即必须能在编译时计算出结果。
2、任何局部对象、局部变量以及它们的地址都不是一个常量表达式,不能用作非类型模板形参的实参。而全局指针类型、全局变量、全局对象也不是常量表达式也不是常量表达式。
3、sizeof运算符的运算结果是一个常量表达式,可以用作非类型模板形参的实参。
4、非类型模板形参一般不用于函数模板,例如:
1 | template<typename T,int a> |
如果调用func(2),则会出现错误,因为编译器无法确定a的值,应该用显式模板形参来解决,即func<int,3>(2)调用。
4.1.3、模板形参
模板形参,也就是模板的参数是另外一个模板,其声明如下:
1 | template<typenam T,template<typename U,typename V> class A> |
上述代码中声明的第二个模板参数就是一个类模板形参。
注意:只有类模板可以作为模板形参,而参数中的关键字class是必需的。
(即知道模板可以作为模板的一个参数就行)
4.2、类模板的派生
类模板和普通类一样,也可以派生,并且分为三种:类模板派生普通类、类模板派生类模板、普通类派生类模板。
4.2.1、类模板派生普通类
在C++中,可以使用任意一个类模板派生一个普通类,但是在派生中,作为非模板类的基类,必须是类模板实例化之后的模板类。即:
1 | template<typename T> |
即类模板A派生出了普通类B,其中类模板A先实例化出一个double类型的具体模板类,再有这个模板类取派生出类B。根据这个派生关系可以从类模板中创建出相对于的类,而不是让编译器自动创建那些无用的类。
4.2.2、类模板派生类模板
从类模板中派生出一个新得类模板,和普通类之间的派生关系几乎一样,例如:
1 | template<typename T> |
类模板B是由类模板A派生出来的,B的数据成员和成员函数类型仍然由类模板参数U决定,因此B仍然是一个模板。
4.2.3、普通类派生类模板
从普通类派生出类模板可以把现存类库中的类转换为通用的类模板。即可以从现存类中创建类模板,也可以创建计语非类模板库的类模板。
1 | class A |
4.3、模板特化
特化就是将泛型的东西具体化。模板特殊就是为已有的模板参数进行一些使其特殊化的指定,使得以前不受任何约束的模板参数,受到特定约束或完全被指定下来。
模板特化一般用于当需要模板的某个特定类型来进行特别处理的时候。并且模板特化分为偏特化和全特化。
4.3.1、偏特化
偏特化就是模板中的模板参数没有被全部确定,需要编译器在编译时进行确定。例如:
1 | template<typename T,typename U> |
将其中一个模板形参进行特化为int类型。
1 | template<typename T> |
注意:函数模板并不支持偏特化。
4.3.2、全特化
全特化就是模板中的模板参数全部被指定为确的的类型。其标志就是产生出完全确定的东西,例如:
1 | template<class T> |
将其全部特化为float数据类型。
1 | template<> |
在上述代码中,template<>已经没有模板形参,只是告诉编译器这是一个特化以后的模板,即模板特化之后就可以处理特殊情况。
注意:函数模板也支持全特化,即全部数据类型特殊指定。(类似于显式具体化)
以下为类模板与函数模板特化例子,例如:
1 |
|
4.3.3、模板特化为引用、指针与模板类型(相对特化、半特化)
之前的特化都是将模板特化为绝对类型,除此之外,我们还可以将模板特化为引用、指针与模板类型,例如:
1 | template<class T> |
这种特化就不是一种绝对的特化,它只是对类型做了部分限定。
1 | template<class T> |
以上特化也不是绝对特化,只是限定为vector类型,具体是vector