C++程序设计:模板
模板是C++支持参数化多态的工具,是泛型编程的基础。模板可以实现类型参数化,即把类型定义为参数,真正实现了代码的可重用性,减少了编程及维护的工作量,并且降低了编程难度。模板是类或函数在编译时定义所需处理和返回的数据类型。一个模板是类或函数的描述,即模板分为函数模板和类模板,本章将针对函数模板和类模板的相关知识进行详细讲解。
一、模板的概念
在C++程序中,声明变量、函数、对象等实体时,程序设计者需要指定数据类型,让编译器在程序运行之前进行类型检查并分配内存,以提高程序运行的安全性和效率。但是这种强类型的编程方式往往会导致程序设计者为逻辑结构相同而具体数据类型不同的对象编写模式一致的代码。例如,定义一个求和函数add(int,int),add()函数可以计算两个int类型数据的和,但是对于double类型的数据就无能为力了,此时,程序设计者还需要定义一个函数add(float,float),计算两个double类型的数据之和,但是这样不利于程序的扩充和维护。
为此,C++标准提供了模板机制,用于定义数据类型不同但逻辑结构相同的数据对象的通用行为。在模板中,运算对象的类型不是实际的数据类型,而是一种参数化的类型。带参数类型的函数称为函数模板,带参数类型的类称为类模板。例如,定义函数add(),计算两个数之和,可以将类型参数化,如add(T,T),其中,T就是参数化的类型,在调用add()函数时,可以传入任意类型的数据,函数可以根据传入的数据推导出T的值是哪种数据类型,从而进行相应的计算。这样程序设计者就可以专注于逻辑代码的编写,而不用关心实际具体的数据类型。
程序运行时,模板的参数由实际参数的数据类型决定,编译器会根据实际参数的数据类型生成相应的一段可运行代码,这个过程称为模板实例化。函数模板生成的实例称为模板函数,类模板生成的实例称为模板类。
二、函数模板
函数模板是函数的抽象,它与普通函数相似,唯一的区别就是函数参数的类型是不确定的,函数参数的类型只有在调用过程中才被确定。
2.1、函数模板的定义
如果定义一个实现两个数相加的函数add(),要实现int、float、double等多种类型的数据相加,则要定义很多个函数,这样的程序就会显得非常臃肿。但使用模板就无须关心数据类型,只定义一个函数模板就可以。定义函数模板的语法格式如下所示:
1 |
|
上述语法格式中,template是声明模板的关键字,<>中的参数称为模板参数;typename关键字用于标识模板参数,可以用class关键字代替,class和typename并没有区别。模板参数不能为空,一个函数模板中可以有多个模板参数,模板参数和普通函数参数相似。tem plate下面是定义的函数模板,函数模板定义方式与普通函数定义方式相同,只是参数列表中的数据类型要使用<>中定义的参数表示。
下面通过案例,演示函数模板的用法,示例如下。
1 |
|
上述代码,第4~8行代码定义了函数模板add(),用于实现两个数据相加。第13~15行代码调用add()函数,分别传入两个int类型数据和两个double类型数据。
当调用add()函数传入int类型参数1和2时,参数T被替换成int,得到结果为3;当传入double类型参数1.2和3.4时,参数T被替换成double类型,得到结果为4.6。这就避免了为int类型数据定义一个求和函数,再为double类型数据定义一个求和函数的问题,实现了代码复用。
需要注意的是,对函数模板的调用应使用实参推演。例如,只能进行add(2,3)这样的调用,或者先定义整型变量int a=2,b=3,再将变量a、b作为参数,进行add(a,b)这样的调用,编译器会根据传入的实参推演出T为int类型,而不能使用add(int,int)方式,直接将类型传入进行调用。
2.2、函数模板的实例化
函数模板并不是一个函数,它相当于一个模子,定义一次即可使用不同类型的参数来调用该函数模板,这样做可以减少代码的书写,提高代码的复用性和效率。需要注意的是,函数模板不会减少可执行程序的大小,因为编译器会根据调用时的参数类型进行相应的实例化。
所谓实例化,就是用类型参数替换模板中的模板参数,生成具体类型的函数。实例化可分为隐式实例化与显式实例化,下面分别介绍这两种实例化方式。
2.2.1、隐式实例化
隐式实例化,是指根据调用函数模版时,传入的参数的数据类型,确定模板参数T的类型,模板参数的类型是隐式确定的。例如,上节的函数模板add()的调用过程,在第一次调用add()函数模板时,传入的是int类型数据1和2,编译器根据传入的实参,推演出模板参数类型是int,就会根据函数模板,实例化出一个int类型的函数,如下所示:
1 |
|
编译器生成具体类型函数的这一过程就称为实例化,生成的函数称为模板函数。生成int类型的函数后,再传入实参1和2进行运算。同理,当传入double类型的数据时,编译器先根据模板,实例化出如下形式的函数:
1 |
|
这样,每一次调用时,都会根据不同的类型实例化出不同类型的函数,最终的可执行程序的大小并不会减少,只是提高了代码的复用性。
2.2.2、显式实例化
隐式实例化不能为同一个模板参数指定两种不同的类型,如add(1,1.2),函数参数类型不一致,编译器便会报错。这就需要显式实例化解决类型不一致的问题。显式实例化需要指定函数模板中的数据类型,语法格式如下所示:
1 |
|
在该语法格式中,<>中是显式实例化的数据类型,即要实例化出一个什么类型的函数。例如,显示实例化为int类型,则在调用时,不是int类型的数据会转换为int类型再进行计算,如将上节的add()函数模板,显式实例化为int类型,代码如下所示:
1 |
|
下面通过案例演示,函数模板add()显式实例化的用法,示例如下。
1 |
|
上述代码,第12行代码显式声明add()函数模板,指定模板参数类型为int。第17行代码在调用int类型模板函数时,传入了一个字符’B’,则编译器会将字符类型的’B’转换为对应的ASCII码值,然后再与10相加得出结果。实际上就是隐式的数据类型转换。
需要注意的是,对于给定的函数模板,显式实例化的声明在一个文件中只能出现一次,并且在这个文件中必须给出函数模板的定义。由于C++编译器的不断完善,模板实例化的显式声明可以省略,在调用时用<>显式指定要实例化的类型即可。比如,在上例中,如果add(1.2,3.4)
函数调用改为add<int>(1.2,3.4)
调用,则会得出结果4。
2.2.3、显式具体化
显式具体化,是对函数模板的重新定义,具体格式如下所示:
1 |
|
显式实例化只需显式声明模板参数的类型,而不需要重新定义函数模板的实现,但显式具体化需要重新定义函数模板的实现。例如,定义一个用于交换两个数据的函数模板,示例代码如下:
1 |
|
假设,现在有如下结构体定义,示例代码如下:
1 |
|
现在要调换两个学生的id编号,但是又不想交换学生的姓名、成绩等其他信息。此时,就可以用显式具体化解决这个问题,重新定义函数模板只交换结构体的部分数据成员。显式具体化的代码如下所示:
1 |
|
如果函数有多个原型,则编译器在选择函数调用时,非模板函数优先于模板函数,显式具体化模板优先于函数模板,例如下面三种定义:
1 |
|
对于int a, int b
,如果存在swap(a,b)的调用,则优先调用直接定义的函数;如果没有,则优先调用显式具体化,如果两者都没有才会调用函数模板。
2.3、函数模板重载
函数模板可以进行实例化,以支持不同类型的参数,不同类型的参数调用会产生一系列重载函数。比如,前面两次调用add()函数模板,编译器会根据传入参数不同实例化出两个函数,如下所示:
1 |
|
此外,函数模板本身也可以被重载,即名称相同的函数模板可以具有不同的函数模板定义,当进行函数调用时,编译器根据实参的类型与个数,决定调用哪个函数模板,实例化函数。
下面通过案例演示,函数模板重载的用法,示例如下。
1 |
|
上述代码,第5~8行代码定义了一个函数max(),用于比较两个int类型数据的大小。第11~15行代码定义了函数模板max(),用于比较两个数的大小。第18~22行代码定义了函数模板max(),用于比较三个数的大小。
第27~33行代码分别传入不同的参数调用函数max()。在调用的过程中,如果参数相同,那么优先调用非模板函数而不会用模板产生实例。例如,第27行代码调用max()函数,传入两个int类型参数,很好地匹配了非模板函数。如果函数模板能够实例化出一个更匹配的函数,则调用时将选择函数模板。例如,第31行代码调用max()函数,利用函数模板实例化一个带有两个char类型参数的函数,而不会调用非模板函数max(int,int)。
需要注意的是,模板不允许自动类型转化,如果有不同类型参数,只能使用非模板函数,因为普通函数可以进行自动类型转换,所以第33行代码调用max()函数时,调用的是非模板函数,将3.2转换成了int类型再与6进行比较。
2.4、注意事项
函数模板虽然可以极大地解决代码重用的问题,但在使用时需注意以下几个方面:
(1)<>中的每一个类型参数,在函数模板参数列表中,必须至少使用一次。例如,下面的函数模板声明,是不正确的。函数模板声明了两个参数T1与T2,但在使用时只使用了T1,没有使用T2。
1 |
|
(2)在全局作用域中声明的,与模板参数同名的对象、函数或类型,在函数模板中将被隐藏。例如:下面的函数模板,在函数体内访问的num是T类型的变量num,而不是全局int类型的变量num。
1 |
|
(3)函数模板中声明的对象或类型不能与模板参数同名。例如:
1 |
|
(4)模板参数名在同一模板参数列表中,只能使用一次,但可在多个函数模板声明或定义之间重复使用。例如:
1 |
|
(5)模板的定义和多处声明所使用的模板参数名不是必须相同。例如:
1 |
|
(6)如果函数模板中有多个模板参数,则每个模板参数前都必须使用关键字class或typename修饰。例如:
1 |
|
三、类模板
对于类来说,也可以定义模板。类模板是对成员数据类型不同的类进行抽象,它不是一个具体实际的类,而是一个类型的类,一个类模板可以生成多种具体的类。
3.1、类模板的定义
类模板的定义格式如下所示:
1 |
|
类模板中的关键字含义与函数模板相同。需要注意的是,类模板的模板参数不能为空。一旦声明类模板,就可以用类模板的参数名声明类中的成员变量和成员函数,即在类中使用数据类型的地方都可以使用模板参数名来声明。定义类模板,示例代码如下所示:
1 |
|
上述代码中,在类A中声明了两个T类型的成员变量a和b,还声明了一个返回值类型为T并带两个T类型参数的成员函数func()。
3.2、类模板的实例化
定义了类模板就要使用类模板创建对象以及实现类中的成员函数,这个过程其实也是类模板实例化的过程,实例化出的具体类称为模板类。如果用类模板创建类的对象,例如,用上述定义的类模板A创建对象,则需要在类模板A后面加上一个<>,并在里面表明相应的类型,示例代码如下所示:
1 |
|
这样类A中凡是用到模板参数的地方都会被int类型替换。如果类模板有多个模板参数,创建对象时,多个类型之间要用逗号分隔开。例如,定义一个有两个模板参数的类模板B,然后用B创建类对象,示例代码如下所示:
1 |
|
使用类模板时,必须要为模板参数显式指定实参,不存在实参推演过程,也就是说不存在将整型值10推演为int类型再传递给模板参数的过程,必须要在<>中指定int类型,这一点与函数模板不同。
下面通过案例演示,类模板的实例化,示例如下。
1 |
|
上述代码,第5~14行代码定义了一个类模板Array,Array的构造函数有一个数组类型参数。第40行代码在创建类对象a1时,用char类型的数组去初始化,调用show()函数输出数组元素。第44行代码创建对象a2时,用int类型的数组去初始化,调用show()函数输出数组元素。
需要注意的是,类模板在实例化时,带有模板参数的成员函数并不会跟着实例化,这些成员函数只有在被调用时才会被实例化。
3.3、类模板的派生
类模板和普通类一样也可以继承和派生,以实现代码复用。类模板的派生一般有三种情况:类模板派生普通类、类模板派生类模板、普通类派生类模板。这三种派生关系可以解决很多实际问题。下面针对这三种派生关系进行讲解。
3.3.1、类模版派生普通类
在C++中,可从任意一个类模板派生一个普通类。在派生过程中,类模板先实例化出一个模板类,然后以这个模板类作为基类派生出普通类。类模板派生普通类的示例代码如下所示:
1 |
|
在上述代码中,类模板Base派生出了普通类Derive,其实在这个派生过程中类模板Base先实例化出了一个double类型的模板类,然后由这个模板类派生出普通类Derive,因此在派生过程中需要指定模板参数类型。
3.3.2、类模板派生类模版
类模板也可以派生出一个新的类模板,它和普通类之间的派生几乎完全相同。但是,派生类模板的模板参数受基类模板的模板参数影响。例如,由类模板Base派生出一个类模板Derive,示例代码如下:
1 |
|
上述代码中,类模板Derive由类模板Base派生,Derive的部分成员变量和成员函数类型由类模板Base的参数T确定,因此Derive仍然是一个模板。类模板派生类模板技术可以用来构建类模板的层次结构。
3.3.3、普通类派生类模版
普通类也可以派生类模板,普通类派生类模板可以把现存类库中的类转换为通用的类模板,但在实际编程中,这种派生方式并不常用,这里只是对它作一个简单示例,只需要了解即可。普通类派生类模板示例代码如下所示:
1 |
|
在上述代码中,类Base是普通类,类模板Derive继承了普通类Base。利用这种技术,程序设计者能够从现存类中创建类模板,由此可以创建基于非类模板库的类模板。
3.4、类模板与友元函数
在类模板中声明友元函数有三种情况:非模板友元函数、约束模板友元函数和非约束模板友元函数。接下来,将针对这三种友元函数进行详细讲解。
3.4.1、非模版友元函数
非模板友元函数,是指在类模板中将一个普通函数声明为友元函数。如下示例:
1 |
|
在类模板A中,将普通函数func()声明为友元函数,则func()函数是类模板A所有实例的友元函数。上述代码中,func()函数为无参函数。除此之外,还可以将带有模板类参数的函数声明为友元函数,示例代码如下:
1 |
|
在上述代码中,show()函数并不是函数模板,只是有一个模板类参数。调用带有模板类参数的友元函数时,友元函数必须显式具体化,指明友元函数要引用的参数的类型,例如:
1 |
|
上述代码中,模板参数为int类型的show()函数是A<int>
类的友元函数,模板参数为double类型的show()函数是A<double>
类的友元函数。
下面通过案例演示,非模板友元函数的用法,示例如下。
1 |
|
上述代码,第4~18行代码定义了类模板A,在类模板A中声明了两个友元函数func()和show()。其中,func()函数为无参友元函数,show()函数有一个模板类对象作为参数。此外,类模板A还声明了静态成员变量_count,用于记录每一种模板类创建的对象个数。
第25~29行代码是func()函数的定义,func()函数的作用是输出A<int>
类对象和A<double>
类对象的个数。第32~35行代码分别定义了show(const A<int>&a)
函数和show(const A<double>&a)
函数,用于分别输出A<int>
类对象和A<double>
类对象的值。
在main()函数中,第40行代码调用func()函数,此时还未创建任何模板类对象,因此,A<int>
类对象和A<double>
类对象的个数均为0。第43行代码创建了A<int>
模板类对象a,初始化值为10。第44行再次调用func()函数,此时,A<int>
类对象的个数为1,A<double>
类对象的个数为0。第46行代码创建A<double>
模板类对象b,初始化值为1.2。第47行再次调用func()函数,此时,A<int>
类对象的个数为1,A<double>
类对象的个数为1。第49~50行代码调用show()函数,分别传入对象a和对象b作为参数,程序会输出对象a和对象b的值。
3.4.2、约束模版友元函数
约束模板友元函数,是指将函数模板声明为类模板的友元函数,但函数模板的模板参数受类模板影响,即函数模板的实例化类型取决于类模板被实例化时的类型,类模板实例化时会产生与之匹配的具体化友元函数。
在使用约束模板友元函数时,首先需要在类模板的定义前,声明函数模板。例如,有两个函数模板声明,示例代码如下:
1 |
|
声明函数模板之后,在类模板中将函数模板声明为友元函数。在声明友元函数时,函数模板要实现具体化,即函数模板的模板参数要与类模板的模板参数保持一致,以便类模板实例化时产生与之匹配的具体化友元函数。示例代码如下所示:
1 |
|
在上述代码中,将函数模板func()与show()声明为类的友元函数,在声明时,func()与show()的模板参数受类模板A的模板参数约束,与类模板的模板参数相同。当生成A<int>
模板类时,会生成与之匹配的unc<int>()
函数和show<int>()
函数作为友元函数。需要注意的是,在上述代码中,func()函数模板没有参数,必须使用<>指定具体化的参数类型。show()函数模板有一个模板类参数,编译器可以根据函数参数推导出模板参数,因此show()函数模板具体化中<>可以为空。
下面通过案例演示,约束模板友元函数的用法,示例如下。
1 |
|
上述代码,第5~10行代码声明函数模板func()和show()。第23~25行代码分别将函数模板func()与show()声明为类的友元函数。第32~37行代码是函数模板func()的定义,用于输出某一类型模板类的大小及对象个数。第40~41行代码是函数模板show()的定义,用于输出模板类对象的值。
在main()函数中,第46行代码调用func<int>()
,即输出A<int>
类的大小及对象个数。A<int>
类的大小为4,对象个数为0。第48~52行代码分别定义A<int>
类对象a和b,A<double>
类对象c。第54~58行代码调用show()函数,分别传入对象a、b、c作为参数,输出各对象的值。第60行代码调用func<int>()
函数,由于此时已经创建了两个A<int>
类对象a和b,因此输出的对象个数应当为2。第62行代码调用func<double>()
函数,由于此时已经创建了一个A<double>
类对象c,因此输出的对象个数应当为1。
3.4.3、非约束模版友元函数
非约束模板友元函数,是指将函数模板声明为类模板的友元函数,但函数模板的模板参数不受类模板影响,即友元函数模板的模板参数与类模板的模板参数是不同的。声明非约束模板友元函数示例代码如下所示:
1 |
|
在上述代码中,类模板A将函数模板show()声明为友元函数,但show()的模板参数U、V不受类模板A的模板参数T影响,则函数模板show()就是类模板A的非约束友元函数。函数模板show()的每个模板函数都是类模板A每个模板类的友元函数。
下面通过案例演示,非约束模板友元函数的用法,示例如下。
1 |
|
上述代码,第5~15行代码定义类模板A,在类模板中将函数模板show()声明为非约束友元函数。第18~19行代码是函数模板show()的定义。第24~28行代码分别定义A<int>
类对象a和b,A<double>
类对象c。第31行代码调用show()函数,传入对象a、b作为实参;第34行代码调用show()函数,传入对象a、c作为实参。
这里,我们需要知道的是,非约束模板友元函数的模板参数与类模板的模板参数不相关,它可以接受任何类型的参数。
四、模板的参数
模板是C++支持参数化多态的工具,模板的参数有三种类型:类型参数、非类型参数和模板类型参数。本节就针对这三种模板参数进行详细讲解。
4.1、类型参数
由class或者typename标记的参数,称为类型参数。类型参数是使用模板的主要目的。例如,下列模板声明:
1 |
|
上述代码中,T就是一个类型参数,类型参数的名称由用户自行确定,表示的是一个未知类型。模板的类型参数可以作为类型说明符用在模板中的任何地方,它与内置类型说明符或类类型说明符的使用方式完全相同。
可以为模板定义多个类型参数,也可以为类型参数指定默认值,示例代码如下所示:
1 |
|
在上述代码中,设置类型参数U的默认值为int类型,类模板的类型参数默认值和普通函数默认参数规则一致。
4.2、非类型参数
非类型参数是指内置类型参数。例如,定义如下模板:
1 |
|
上述代码中,int a就是非类型的模板参数,非类型模板参数为函数模板或类模板预定义一些常量,在模板实例化时,也要求实参必须是常量,即确切的数据值。需要注意的是,非类型参数只能是整型、字符型或枚举、指针、引用类型。
非类型参数在所有实例中都具有相同的值,而类型参数在不同的实例中具有不同的值。
下面通过案例演示,非类型参数的用法,示例如下。
1 |
|
上述代码,第5~19行代码定义了类模板Array,类模板Array的第二个参数为非类型参数unsigned len。第12~18行代码重载了“[]”运算符,实现通过下标访问数组元素。在main()函数中,第24~26行代码实例化类模板Array时,分别定义了长度为5和10的两个数组。第二个参数都是具体的数值,解决了常量参数只能固定大小的问题。当需要为同一算法或类定义不同常量时,最适合用非类型参数实现。
使用非类型参数时,有以下几点需要注意。
- 调用非类型参数的实参必须是常量表达式,即必须能在编译时计算出结果。
- 任何局部对象、局部变量的地址都不是常量表达式,不能用作非类型的实参,全局指针类型、全局变量也不是常量表达式,也不能用作非类型的实参。
- sizeof()表达式结果是一个常量表达式,可以用作非类型的实参。
- 非类型参数一般不用于函数模板。
4.3、模版类型参数
模板类型参数,是指模板的参数是另一个模板,声明格式如下所示:
1 |
|
上述代码中,类模板Parameter的第二个模板参数就是一个类模板。需要注意的是,只有类模板可以作为模板参数,参数声明中必须要有关键字class。
五、模板特化
特化,是指将泛型的东西具体化,模板特化就是为已有的模板参数进行具体化指定,使得不受任何约束的模板参数受到特定约束或完全被指定。
通过模板特化可以优化基于某种特定类型的实现,或者克服某种特定类型在实例化模板时出现的不足,如某类型没有提供某种操作。例如,有以下类模板定义:
1 |
|
上述代码中,类模板Special定义了一个成员函数compare(),用于比较两个成员变量_a和_b的大小。如果将其实例化为Special<const char*>
类,由于const char*
类型没有提供“>”运算操作,因此,Special<const char*>
类对象调用compare()函数时,比较的是两个字符串的地址大小,这显然是没有意义的。为了解决const char*
特殊类型所产生的问题,可以将类模板特化。
模板特化可分为全特化与偏特化,下面分别进行介绍。
5.1、全特化
全特化,是指将模板中的模板参数全部指定为确定的类型。对于类模板,类中所有成员函数都要进行特化。
进行类模板特化时,需要将类的成员函数重新定义为普通成员函数。在全特化时,首先使用template<>进行全特化声明,然后重新定义需要全特化的类模板,并指定特化类型。
下面通过案例演示,类模板的全特化,示例如下。
1 |
|
上述代码,第6~23行代码定义了类模板Special。第26~48行代码是类模板Special针对const char*
数据类型的全特化,其中的第40~47行代码重新定义成员函数compare()为普通成员函数,实现const char*
类字符串的比较。
第53~54行代码定义Special<string>
类对象s1,并调用compare()函数比较两个字符串大小;第56~57行代码定义Special<const char*>
类对象s2,并调用compare()函数比较两个字符串大小。在这里,对象s1调用的是类模板实例化出的compare()函数,对象s2调用的是全特化之后的compare()函数。
5.2、偏特化
偏特化,是指模板中的模板参数没有被全部指定,需要编译器在编译时进行确定。例如,定义一个类模板,示例代码如下所示:
1 |
|
将其中一个模板参数特化为int类型,另一个参数由用户指定,示例代码如下所示:
1 |
|
六、参考
[《C++程序设计教程》](《C++程序设计教程(第2版)》电子书在线阅读-黑马程序员 编著-得到APP (dedao.cn))