C++ 模板简介
一、模板
使用模板的目的就是能够让程序员编写与类型无关的代码。
模板是一种对类型进行参数化的工具,通常有两种形式:函数模板和类模板。函数模板针对仅参数类型不同的函数;类模板针对仅数据成员和成员函数类型不同的类。
注意:
模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。
二、函数模板
函数模板定义了参数化的非成员函数,这使得程序员能够用不同类型的参数调用相同的函数,由编译器决定调用哪一种类型,并且从模板中生成相应的代码。
定义一个函数模板:
template <class 形参名, class 形参名, ...>
返回类型 函数名(参数列表)
{ ... }
其中 template 和 class 是关键字,class 可以用 typename 关键字代替,在这里 typename 和 class 没区别,<>括号中的参数叫模板形参,模板形参和函数形参很相像,模板形参不能为空。一但声明了模板函数就可以用模板函数的形参名声明类中的成员变量和成员函数,即可以在该函数中使用内置类型的地方都可以使用模板形参名。模板形参需要调用该模板函数时提供的模板实参来初始化模板形参,一旦编译器确定了实际的模板实参类型就称他实例化了函数模板的一个实例。
三、类模板
1. 类模板定义及实例化
定义一个类模板:
template <class 形参名, class 形参名, ...>
class 类名
{ ... };
其中,template 是声明类模板的关键字,表示声明一个模板,模板参数可以是一个,也可以是多个,可以是类型参数 ,也可以是非类型参数。类型参数由关键字 class 或 typename 及其后面的标识符构成。非类型参数由一个普通参数构成,代表模板定义中的一个常量。
注意:
(1) 如果在全局域中声明了与模板参数同名的变量,则该变量被隐藏掉。
(2) 模板参数名不能被当作类模板定义中类成员的名字。
(3) 同一个模板参数名在模板参数表中只能出现一次。
(4) 在不同的类模板或声明中,模板参数名可以被重复使用。
(5) 在类模板的前向声明和定义中,模板参数的名字可以不同。
(6) 类模板参数可以有缺省实参,给参数提供缺省实参的顺序是先右后左。
(7) 类模板名可以被用作一个类型指示符。当一个类模板名被用作另一个模板定义中的类型指示符时,必须指定完整的实参表
类型参数和非类型参数
-
1. 类型参数:类型参数由关键字 class 或 typename 后接说明符构成,如 template <class T> void h(T a){};其中 T 就是一个类型参数,类型参数的名字由用户自已确定。类型参数表示的是一个未知的类型。类型参数可作为类型说明符用在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同,即可以用于指定返回类型,变量声明等。
- 1.1 针对函数模板,不能为同一个模板类型形参指定两种不同的类型,比如template <class T>void h(T a, T b){},语句调用 h(2, 3.2) 将出错,因为该语句给同一模板形参 T 指定了两种类型,第一个实参 2 把模板形参 T 指定为 int,而第二个实参 3.2 把模板形参指定为 double,两种类型的形参不一致,会出错。
- 1.2 针对类模板,当我们声明类对象为:A<int> a,比如 template <class T>T g(T a, T b){},语句调用 a.g(2, 3.2) 在编译时不会出错,但会有警告,因为在声明类对象的时候已经将T转换为 int 类型,而第二个实参 3.2 把模板形参指定为 double,在运行时,会对 3.2 进行强制类型转换为 3 。当我们声明类的对象为:A<double> a ,此时就不会有上述的警告,因为从 int 到 double 是自动类型转换。
-
2. 非类型参数:模板的非类型形参也就是内置类型形参,如template<class T, int a> class B{};其中int a就是非类型的模板形参。
- **2.1 **非类型模板的形参只能是整型,指针和引用。
- **2.2 **调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果。
- **2.3 **任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。
- **2.4 **全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,可以用作非类型模板形参的实参。
- **2.5 **sizeof表达式的结果是一个常量表达式,也能用作非类型模板形参的实参。
- **2.6 **非类型形参一般不应用于函数模板中,比如有函数模板template<class T, int a> void h(T b){},若使用 h(2) 调用会出现无法为非类型形参 a 推演出参数的错误,对这种模板函数可以用显示模板实参来解决,如用 h<int, 3>(2) 这样就把非类型形参 a 设置为整数 3。
- **2.7 **非类型模板形参的形参和实参间所允许的转换:
1、允许从数组到指针,从函数到指针的转换。如:
template <int *a> class A{}; int b[1]; A <b> m;
即数组到指针的转换
2、const 修饰符的转换。如:template<const int *a> class A{}; int b; A<&b> m; 即从int *到const int *
的转换。
3、提升转换。如:template<int a> class A{}; const short b=2; A<b> m;
即从 short 到 int 的提升转换
4、整值转换。如:template<unsigned int a> class A{}; A<3> m;
即从 int 到 unsigned int 的转换。
5、常规转换。
注意:
(1) 可以为类模板的类型形参提供默认值,但不能为函数模板的类型形参提供默认值。函数模板和类模板都可以为模板的非类型形参提供默认值。
(2) 类模板的类型形参默认值形式为:template<class T1, class T2=int> class A{};
为第二个模板类型形参 T2 提供 int 型的默认值。
(3) 类模板类型形参默认值和函数的默认参数一样,如果有多个类型形参则从第一个形参设定了默认值之后的所有模板形参都要设定默认值,比如template<class T1=int, class T2>class A{};
就是错误的,因为 T1 给出了默认值,而 T2 没有设定。
(4) 在类模板的外部定义类中的成员时 template 后的形参表应省略默认的形参类型。比如template<class T1, class T2=int> class A{public: void h();};
定义方法为template<class T1,class T2> void A<T1,T2>::h(){}
。
类模板实例化
定义:从通用的类模板定义中生成类的过程称为模板实例化。
类模板什么时候会被实例化呢?
① 当使用了类模板实例的名字,并且上下文环境要求存在类的定义时。
② 对象类型是一个类模板实例,当对象被定义时。此点被称作类的实例化点。
③ 一个指针或引用指向一个类模板实例,当检查这个指针或引用所指的对象时。
template<class Type>
class Graphics{};
void f1(Graphics<char>);// 仅是一个函数声明,不需实例化
class Rect
{
Graphics<double>& rsd;// 声明一个类模板引用,不需实例化
Graphics<int> si;// si是一个Graphics类型的对象,需要实例化类模板
}
int main(){
Graphcis<char>* sc;// 仅声明一个类模板指针,不需实例化
f1(*sc);//需要实例化,因为传递给函数f1的是一个Graphics<int>对象。
int iobj=sizeof(Graphics<string>);//需要实例化,因为sizeof会计算Graphics<string>对象的大小,为了计算大小,编译器必须根据类模板定义产生该类型。
}
2. 类模板的成员函数
要点:
① 类模板的成员函数可以在类模板的定义中定义(inline 函数),也可以在类模板定义之外定义(此时成员函数定义前面必须加上 template 及模板参数)。
② 类模板成员函数本身也是一个模板,类模板被实例化时它并不自动被实例化,只有当它被调用或取地址,才被实例化。
3. 类模板的友元声明
非模板友元类或友元函数
顾名思义,第一种声明表示具体的类或函数。
绑定的友元类模板或函数模板
第二种声明表示类模板的实例和它的友元之间是一种一对一的映射关系。
如图:
非绑定的友元类模板或函数模板
第三种声明表示类模板的实例和它的友元之间是一种一对多的映射关系。
如图:
注意:当把非模板类或函数声明为类模板友元时,它们不必在全局域中被声明或定义,但将一个类的成员声明为类模板友元,该类必须已经被定义,另外在声明绑定的友元类模板或函数模板时,该模板也必须先声明。
4. 类模板的静态数据成员、嵌套类型
类模板的静态数据成员
要点:
① 静态数据成员的模板定义必须出现在类模板定义之外。
② 类模板静态数据成员本身就是一个模板,它的定义不会引起内存被分配,只有对其实例化才会分配内存。
③ 当程序使用静态数据成员时,它被实例化,每个静态成员实例都与一个类模板实例相对应,静态成员的实例引用要通过一个类模板实例。
类模板的嵌套类型
要点:
① 在类模板中允许再嵌入模板,因此类模板的嵌套类也是一个模板,它可以使用外围类模板的模板参数。
② 当外围类模板被实例化时,它不会自动被实例化,只有当上下文需要它的完整类类型时,它才会被实例化。
③ 公有嵌套类型可以被用在类定义之外,这时它的名字前必须加上类模板实例的名字。
5. 成员模板
定义:成员定义前加上 template 及模板参数表。
要点:
① 在一个类模板中定义一个成员模板,意味着该类模板的一个实例包含了可能无限多个嵌套类和无限多个成员函数。
② 只有当成员模板被使用时,它才被实例化。
③ 成员模板可以定义在其外围类或类模板定义之外。
注意:类模板参数不一定与类模板定义中指定的名字相同。
6. 类模板的特化及部分特化
类模板的特化
先看下面的例子:
Template<class type>
Class Graphics
{
Public:
void out(type figure){…}
};
Class Rect{…};
如果模板实参是 Rect 类型,我们不希望使用类模板 Graphics 的通用成员函数定义,来实例化成员函数 out(),我们希望专门定 Graphics<Rect>::out() 实例,让它使用 Rect 里面的成员函数。
为此,我们可以通过一个显示特化定义,为类模板实例的一个成员提供一个特化定义。
格式:template<> 成员函数特化定义
下面为类模板实例Graphics<Rect>的成员函数out()定义了显式特化:
Template<> void Graphics<Rect>::out(Rect figure){…}
注意:
① 只有当通用类模板被声明后,它的显式特化才可以被定义。
② 若定义了一个类模板特化,则必须定义与这个特化相关的所有成员函数或静态数据成员,此时类模板特化的成员定义不能以符号 template<> 作为打头。(template<> 被省略)
③ 类模板不能够在某些文件中根据通用模板定义被实例化,而在其他文件中却针对同一组模板实参被特化。
类模板部分特化
如果模板有一个以上的模板参数,则有些人就可能希望为一个特定的模板实参或者一组模板实参特化类模板,而不是为所有的模板参数特化该类模板。即,希望提供这样一个模板:它仍然是一个通用的模板,只不过某些模板参数已经被实际的类型或值取代。通过使用类模板部分特化,可以实现这一点。
template<int hi,int wid>
Class Graphics{…};
Template<int hi>//类模板的部分特化
Class Graphics<hi,90>{…};
格式:template<模板参数表>
注意:
① 部分特化的模板参数表只列出模板实参仍然未知的那些参数。
② 类模板部分特化是被隐式实例化的。编译器选择 “针对该实例而言最为特化的模板定义” 进行实例化,当没有特化可被使用时,才使用通用模板定义。
例:Graphics<24,90> figure;
它即能从通用类模板定义被实例化,也能从部分特化的定义被实例化,但编译器选择的是部分特化来实例化模板。
③类模板部分特化必须有它自己对成员函数、静态数据成员和嵌套类的定义。
7. 名字空间和类模板
类模板定义也可以被放在名字空间中。例如:
Namespace cplusplus_primer
{
Template<class type>
Class Graphics{…};
Template<class type>
Type create()
{…}
}
当类模板名字 Graphics 被用在名字空间之外时,它必须被名字空间名 cplusplus_primer 限定修饰,或者通过一个 using 声明或指示符被引入。例如:
Void main()
{
using cplusplus_primer::Graphics;
Graphics<int> *pg=new Graphics<int>;
}
注意:在名字空间中声明类模板也会影响该类模板及其成员的特化和部分特化声明的方式,类模板或类模板成员的特化声明必须被声明在定义通用模板的名字空间中(可以在名字空间之外定义模板特化)。
** 参考资料: **
C++中的类模板详细讲述
C++ 模板详解(一)
C++ 模板详解(二)