大家好,在这一小节中啊,我们来进一步学习一下类模板。
我们在上一小节中呢,已经知道说,众多个函数可以被抽象成一个函数模板。
那么类呢,它也可以进一步,虽然累这个概念本身就是从若干个对象中间呢,抽象出来的,
那么,若干个类呢,还可以把它也写成一个模板,这样程序呢,会进一步地简化。
我没来看一下。那么类模板的提出呢,其实即使希望能够去定义一批相似的类。
那么通过去定义一批相似的类呢,就可以生产一个叫作类模板的概念。
那么有了这个类模板之后呢,具体我通过去实例化不同的这个
相应的参数,就可以得到、生成若干个不同的类来。
我们来具体看一下。
我们呢可能会去定义一个类,它表针呢是一个数 组啊。这个数组呢它是一种非常常见的数据元素,
那么这个数据类型本身呢,它的元素呢可以是整型
的,也可以是某一个学生类啊,也可以是字符串。
那么这样一系列若干个不同的数组,怎么样能够 去能够以一个呃,统一的形式来进行表达呢?
我们就需要去定义一个称为叫作数组类,
啊。这个数组类呢,它自身能够提供一些相应的基本操作。
比方说呢,你可以去查看数组的长度 len, 你可以去获得其中的一个元素getElement
啊,只要通过传入具体的一个 index 信息就可以。
你也可以去 set 其中的一个元素啊,对它进行赋值。
那么有了这样的一系列的这些,这个操作的时候呢,你会发现说,我的这个具体的数组本身,
它实际上是可以作用在很多若干个不同的数据类型上的。
那么我就可以通过对这个数组的这个类呢,来进行进一步的抽象,来得到一个类模板。
那么要注意就是,我们在使用类模板的时候呢,
我们要给这个相应的类,它定义的时候,会给它一个或者多个的参数。
那么这些参数呢,它四可以表示为不同的数据结构的,啊,比方说以
刚才这个数组类,那么这个数组类本身,它的元素类型呢,
是不同的,但是呢,它的其它的这些内容都是完全相同的。
我们在去调用类模板的时候呢,通过去指定参数,
相应的编译器呢,就会去根据参数,具体地对 相应的这个数据类型呢,实现自动地这个生成。
那么有了这样的这个过程呢, 自动生成相应的这个,把它生成为出来的这个类呢,
就称为叫作模板类,啊。也就是说,我们通过类模板
具体指定参数之后,由编译器生成的那个相应的类,我们称为叫作
模板类。那么在C++当中呢,具体的这个类模板
的写法呢,其实跟我们之前看到的这个函数模板非常类似。
我们首先也还是利用 template 这关键字呢去声明一系列的这个类型参数表,啊。这个类型
参数表你可以写若干个,只要你希望表达不同的参数,那么都把它写在这个类型参数表里头就可以了。
完了之后呢,利用 class 去声明一个类模板名。
完了之后在这个类模板当中呢,去定义相应的成员函数和成员变量。
那么要注意,我们有些时候呢,会把这个 这个相应的这个成员函数呢,放在这个类模板之外来定义。
那么和之前写类的这个成员函数是一样的。我们就需要在这个
呃,写在外面的这个成员函数前面呢加一个作用域,啊,标记说是这个对应
类模板它所包含的的成员函数名。
那么要注意就是我们需要在这个,对应的类模板名后面要跟上相应的这个类型参数 名列表,啊。
那么要注意就是说,这个具体的参数列表呢, 它可能呢是需要,就如果它本身是包含了这个
对应的参数的话呢,那么我们就需要去定义对象的时候,就要
去利用这个类模板名,后面跟上一系列的这个具体的
真实的类型参数表,来定义相应的这个对象,啊。
完了之后呢,构造函数呢要实际地去传递这个参数表。
那么如果本身你的这个类模板呢是
有这个无参构造函数的,啊,有这个无参的构造函数,那么相应你也可以在定义对象的时候呢,
不去显示的去写这些参数,具体是怎么样去构造的。
我们来看一个具体的例子。我们呢,需要定义一个叫作 Parir 类的这样的一个模板。
这个类模板呢,它包含了两个类型参数,T1 和 T2。
那么我们在这个类里面分别用 T1和 T2 去标记这个 Pair 类里面的一个 关键字
key 和 value ,啊。这个 key 是 T1 类型的,value 呢是 这个 T2 类型的。
有了这样两个这个 key 和 value 之后呢,我们就看到 我们还定义了一个相应的 Pair 的构造函数,啊。
那么它传递的这个 k 和 v 呢,分别也是 T1 和 T2 类型的。
这个 k 和 v 两个参数值就是用来去初始化, key 和 value
的。也就是说在这个构造函数里面,通过 传递这样两个 T1 T2类型的参数, k 和 v
, 分别去初始化这个 T1 类型的 k ,和 T2 类型的 value。
除此之外呢,我还在这个程序里面呢,去定义了一个
这个运算符重载,operator< ,那么这个运算符重载它本身的这个
对应的这个传递的参数呢,也是一个 Pair 类型的一个指针的引用,啊。
p。
那么注意呢,这个 operator< 呢,因为我只是去实现了一个比较大小,它不会去
改变带比较大小的这样对象的这个值,所以呢我会把它定义为是一个 const 类型的。
那么有了这样的一个 Pair 类的一个模板的定义之后,我们就来具体看一看,它的成员
函数是怎么实现的啊。如果我需要在这个类模板外部去实现这个 operator<
的话呢,那么 那么我们刚才强调了,我们需要在前面去标记这样的一个,啊,
具体的类模板的一个作用域,啊,标明说它是属于
Pair 类的这样模板的这样的一个函数。
那么对应呢,也需要去标记相应的类型参数,T1 和 T2 ,啊。
那么实际上我们在这个成员函数里头它所实现的功能呢, 就是去 return
对应的这个 k 以及 p指向的这个 key 中间 更小的那个值。
那么我们来看有了这个 Pair 类的这个模板之后,我具体会怎么样去使用呢?
那么我在这个 main 函数里头,就要去具体地实例化
出一个 类 Pair 的一个这个模板类来。
那么我们刚才看到,我的这个 Pair 的这个类模板当中它有两个参数。
这两个参数呢,这个类型呢分别是 T1 和 T2
。那么我在实例化的时候呢,我就会去相应地去 给它
这个替换为具体的这个数据类型。
在我们这个例子当中呢,T1 就被替换为是 string ,T2 呢就变成了 int。
那么我定义的这个 Pair 呢,它是具体的这个是一个什么样的对象呢?是一个 student 这样的一个对象。
这个对象呢,它呢本身呢实际上呢是包含了
两个类型,两个值,啊。具体的,分别是,用来呢, 一个字串 Tom
来标记这个 student 姓名以及他相应的这个年龄,啊,19。
那么我们看到说,我们通过去指定这个 T1 和 T2
类型,将刚才的这个 Pair 类的这样的一个模板
把它实例化为一个具体的类,那么它就是这个 Pair <string 和 int
, 那么利用这个类呢,就可以具体实例化一个对象 student ,
那么这个 student 这个.k 和 .value 呢,就可以直接去访问和输出了。
所以我们在这个程序当中呢,当然
输出就是 Tom 19,啊,我们相应地去输出我们对应的这个
呃,类模板,相应实例化出的这个模板类 BE 的对象 student。
那么我们在用类模板来声明对象的时候呢,我们注意
到,实际上编译器本身呢,它就是一个由类模板生成类的过程。
我们把这个过程,我们刚才这个 Pair 的这个类模板,生成具体的这个
Pair < string 和 int 这样的一个过程呢,称为叫作类模板的实例化。
那么编译器呢,会自动地去用具体的这个数据类型,
来替换类模板当中我们定义的这个类型参数,
并且呢去生成相应的这个模板类的这个代码,因为我们用类模板实例化之后产生的就是一个模板类。
所以呢,我们说,注意区别类模板实例化得到的
那个类叫做模板类。一个叫做类模板,一个叫做模板类。
那么类模板呢,是一个模板更泛化意义上的一个概念。而模板类,就是
将这个具体的这个类模板实例化得到那个类,就称为叫做模板类。
那么有了这样的一个模板类之后呢,我们就会看到
通过指定不同的这个类型参数的话,就可以相应地去生成 若干个不同的模板类来。
那么我们要强调一下的就是,我们是使用同一个类模板
同一个类模板来生成了两个模板类。虽然它源于同一个类模板,
但是呢,因为它是两个不同的模板类,所以这两个类之间的对象呢,是不兼容的。
比方说,我这里定义了两个, 都是通过Pair这个类模板来进行实例化
的模板类,分别类型呢,是<string, int>和<string, double>。
那么定义呢,一个是对应的指针p,一个是对象a。
那么如果我这个时候,想让我的这个指针p指向a,是不可以的。因为
编译器会发现说,这样两个类是不一样的,虽然它是来源于不同的类模板,
但是它具体的这个替换的参数呢,是不一样的。
那么类模,就是类模板本身呢,它的成员
函数呢,还可以是一个函数的模板。也就是说,我在模板里头又包含了函数模板。
那么我们来看,我们如果写了一个class A,它呢,就是一个类模板,那么它的这个参数呢,
是class T,啊。那么这个类模板本身,它包含了一个
函数,叫做Func。
这个函数本身,它呢,又是一个函数模板。
也就是说,这个函数本身的这个类型呢,也是不固定的。
那么我在这个函数模板里头呢,只需要输出相应的这个t就可以了。
那么要注意的就是,我们要强调一下,
我们在这个程序里面呢,它对应的这个类模板的
类型参数,和这个函数模板的类型参数呢,是不能一致的。
如果呢,你在这个函数模板里头,也去使用了这个class
T来去定义这个 对应的参数类型的话呢,那么编译器会报错。
那么这个程序的话呢,因为我们在这个Class A里面去定义了一个相应的函数模板。
那么如果我具体实例化,生成相应的这个模板类,我把这个t
替换为int的时候,我对应定义了一个变量,对应定义了一个对象a,
那么这个a呢,就可以去调用相应的Func这个函数,这个函数里面的这个
对应的这个t值呢,那么它的类型呢,实际上应该是一个char类型的一个字串。
我相应就是把,对应的这个成员函数Func呢,也实例化了。
那么这样的话呢,我们就可以看到,我们在一个类模板里头,实际上是可以去使用函数模板的。
那么这个程序呢,就会相应地输出这个K。
那么我们要注意,我们在使用类模板的时候,这个
类型参数,本身的这个参数的声明呢,它实际上
中间可以包含一些非类型的参数。就是说,这个参数表里面,
实际上,不一定都要去写相应的类型参数。它还可以包含一些这种常量的值。
那么,不是常量的,它可以包含一些这种这个非类型的参数。
那么对于这个非类型参数来讲的话呢,它其实主要是用来说明这个类模板当中
这个属性的,而这个类型参数呢,是用来说明这个类模板当中的
具体的这个属性类型,成员的操作的一些参数的类型,或者是返回值的类型等等,
那么这两个值呢,本身的作用呢,是不太一样的。
那么我们可以看到,这个类模板
的类型参数表中间如果出现了一些非类型的参数, 比方说,我们这里定义了一个类模板,称为叫做array。
那么这个array呢,它除了定义了class T这样的一个类型参数之外,还包含了一个
int size这样的一个非类型的参数,那么这个int size,它用来标记什么呢?
它其实就是用来去标记这个array的大小,这个size呢,它是一个固定的属性。
所以呢,我们就可以用一个非类型的参数来进行标记。
那么不管对于任何一个数组来讲,每个数组,它虽然里面这个类型的成员, 可能会不一样,
就是成员的类型可能会不一样,但是它对应呢,每一个数组都一定会包含这样的一个个数的一个特性。
那么这个特性,就可以去使用这个非类型参数来进行标记。
那么要注意呢,我们刚才提到,每一个类模板,它在具体实例化的时候,
如果它实例化的这个类型参数的这个类型不一致的话,那么它是两个不同的这个
模板类。那么同样的,如果你的这个非类型
参数的值不一样,那么它对应呢,也是不一样的。
所以呢,我们看到说,这里 当然a2和a3显然是不一样,因为它们传递的这个类型参数,一个是double,一个是int。
但是呢,如果我现在前面两个都是int,只是这个非
类型参数不一致的话呢,它表征的也是,完全 两个不一样的类。
那么在类模板当中呢,它也包含有继承这样的一个方式。
那么类模板的继承呢,它有四种形式。
一种叫做从类模板本身去派生出类模板,
另外呢,你可以从模板类,所谓模板类,我们刚才反复强调,
就是在类模板当中的这个类型,或者非类型的参数实例化之后的这个类。
可以通过模板类本身,来派生出类模板。
当然你也可以从普通类里面呢,派生出类模板。
最后呢,也可以由模板类派生出普通的类来。
所以我们看到说呢,我们有这样的四种形式,分别是类模板派生类模板,
模板类派生类模板, 那么普通类派生类模板,以及这个最后的这个,
模板类派生普通类,这样四种方式。
我们来具体看一下, 在第一种,类模板从类模板中派生的例子呢,
我们可以看到,如果我们定义了一系列的这个类模板, 首先第一个呢,就是class
A,class A里面包含了两个类型参数,分别是T1和T2.
那么T1和T2呢,就分别定义了class A这个类模板当中的两个成员变量,v1和v2。
那么有了这样一个class A这样的一个父类之后呢,我可以呢,派生出来一个class B。
class B它也是一个类模板,它是由这个class A呢,派生出来的。
那么,这个class B呢,它所包含的类型参数,跟class A呢,是一致的,
分别也是T1和T2。那么我们定义呢,这个class
B当中的这个, 除了这个原来既从A里面继承的这些成员变
量之外,它还有v3和v4两个成员变量。
那么进而呢,我们又派生了一个class C,它是从class B里面去派生的。
当然因为class C呢,它自身只有一个类型参数T, 所以呢,我们在去具体实例化这个
类B的时候呢,我们只具体传递了一个
这个参数类型T,它用来定义的呢,是这个
v5,那么有了这样的一系列的这个派生的这个
类模板之后呢,我们来看一下。我们呢,可以去定义
一个由class B这样的一个类模板
通过去将其T1替换成int, T2替换成double,那么得到的这样一个类,
具体的这个模板类,所定义的对象,obj1。
之后呢,我们还可以去定义一个C。
具体的这个C模板类, 类模板定义的这样一个模板类,啊,它的具体的这个实例化对象,obj2.
我们来看,实际上对于这个, class
B来讲,如果把它 定义成取T1替换为
int,T2替换为double的话,那么相应地它实际上也会去替换 它对应的这个
父类,a中间的这两个类型参数,T1和T2。那么也就是说它定义了int型的
实例化了一个int型的一个v3和double型的v4。那么
相应地呢,这个类a呢,也会具体地实例化成 这个double型的v1和int型的v2。
我们看到呢,第二种情况就是让类模板从
模板类里面派生。也就是说,我已经将一个具体
的类从一个类模板里面呢,派生了,实例化了出来。
从这个实例化后的模板类里面,去派生一个类模板。
比方说这里,我们定义了一个, 类模板class
A,那么它包括了两个类型参数T1和T2, 分别去定义相应的成员变量,v1和v2。
那么我可以首先就将这个类模板实例化,变成一个模板类,
这个模板类,对应的T1呢,就是int, T2呢,就是double。那么有了这样的一个实例化后的模板
类之后呢,我又定义了一个类模板,称为叫做class B。
这个class B呢,它包含了一个类型参数T。
可以用这个T呢,具体地去定义其相应地派生类的成员变量v。
那么有了这样的一个对应的定义之后呢,
我们看到,如果我们在main函数里面去定义了一个,
类模板B,具体这个实例化的这个,
模板类,它呢,是将T替换成为char类型的这样一个对象,
obj1的时候,我们实际上呢,会自动地生成两个 模板类,分别是A<int,
double>和B<char>,也就是说 我这个类的对象呢,它本身,是包含了这样两个
模板类所定义的那些一系列的特性。
那么第三种,就比较简单了,它是指说类模板呢,是
从普通类那里派生的。我定义了一个普通类class A,包含了一个int型的
这个成员变量v1,那么它作为父类,又派生出来 一个类模板,class
B,这个class B呢,它除了包含这个 父类里面的这个int
v1之外呢,它还定义了一个这样T类型的一个成员变量v,
那么我也可以直接去实例化,将这个T呢, 实例化成一个char类型的一个这样的一个字符。
那么最后一种呢,就是从普通类,就是让,把它从这个
普通类呢,从这个模板类里面派生出来,也就是说我可以从一个模板类可以派生一个普通类。
那么我定义了一个模板类,class A,那么它包含了这个
成员变量v1呢,是以这个T类型作为这个参数来,
类型参数来定义的,那么我可以呢,通过去
具体的这个实例化,具体的这个模板类,我让这个T1
替换成为是int,这样的一个模板类
,在实例化了之 后呢,就可以作为我的这个普通类class B的一个父类,
来进行这个派生。那么class B呢,就继承了相应的这个
模板类A<int>它所包含的一系列的这个成员
变量这个属性。那么我在这个main函数里面,只需要去
对应的实例化或者定义,具体类B的这个对象
obj1,就可以了。而这个obj1呢,因为它是一个普通类的一个对象,
所以它不需要传递任何的这个类型参数,那么它所包含的这个特征呢,也是具体了这个
模板类A 所包含的这个int v1和int
n以及double v 这样三个成员变量。