读者是否还记得之前在标准库各个容器类定义时所使用的尖括号(<>)呢?这个尖括号以及其中的类型名就是模板的实例化。模板的概念与我们一开始提出的IntContainer和FloatContainer类似,只是将重复书写相似代码的过程交给了编译器来做。我们先创建一个类似于vector的带有模板的类来感受一下:
动手写11.1.1
动手写11.1.1展示了类模板的声明语法及应用。运行结果如图11.1.1所示:
图11.1.1 类模板
这个示例比较复杂,基本上完整实现了一个固定大小的泛型vector的基本功能。我们在使用的时候可以指定元素的类型和容器的容量,这都是要靠模板才能实现的。
模板定义的默认开头是一个“template”关键字和一对尖括号包住的模板形参列表(Template Parameter List),模板形参可以是类型形参(Type Parameter),也可以是非类型形参(Nontype Parameter)。
非类型形参就跟函数参数差不多,但是在编译的时候必须确定,所以在实例化创建对象时一定要用常量表达式。当我们在定义类对象的时候,将5带入capacity,编译器会自动将capacity替换为5,所以我们看到的动态分数组的“arr = new T[capacity];”实际上变成了“arr = new T[5];”。
而类型形参就是泛型的由来,我们在写“typename T”或者“class T”(typename和class没有区别,都能用来定义类型形参)的时候,就是把T当作一种未知类型的占位符。在定义模板类的时候,我们并不知道T代表了什么类型,而只有定义类对象指定类型的时候,编译器才会把T替换为其他类型(包括自定义的类),比如在这里是int。
在我们定义模板类对象之后,编译器所要做的这些替换行为叫作模板实例化(Template Instantiation)。模板实例化有点像预处理器中宏的扩展,模板类的定义就是宏定义,模板参数就是宏的参数。当我们定义“MyVector<int,5>vec;”的时候,编译器就知道在编译的时候它需要创建一个新的类定义,其中所有的T都要替换成int,而capacity都要替换成5。而当我们又定义一个“MyVector<MyClass,10>vec;”的时候,编译器又要再创建一个类定义,其中所有T换成MyClass,而capacity换成10,并且所有的赋值或其他运算符会使用显式或默认的重载运算符。
所以每个模板类的对象定义都是定义了一个新的类,只是我们不需要重复地定义相同功能、不同类型的类。下面我们来看看动手写11.1.1的不用模板的等价版本:(www.xing528.com)
动手写11.1.2
在动手写11.1.2中,我们可以看到这个版本的MyVector非常不灵活,如果我们想再定义一个,就要把整个类定义重写一遍。
需要注意的是,在动手写11.1.1中我们是将成员函数定义写在类定义里面的,而当我们将定义和声明分开的时候,也需要注意类模板的特殊语法。
动手写11.1.3
动手写11.1.3展示了类模板成员函数定义分离的语法。我们可以看到,在类定义外定义成员函数的时候,有两个东西是不能少的:
1.template关键字和模板形参列表。
2.作用域操作符前的类名要加上模板形参。
这是因为这里的成员函数也是类模板声明的一部分,我们必须标注它的模板特性,不然它就是一个特定版本(比如Interval<int>)的成员函数了。
最后,有两个相近的术语是我们经常会碰到的,那就是类模板(Class Template)和模板类(Template Class)。类模板指的是我们写的带模板的类定义,它表示着一个蓝图,编译器可以通过这个蓝图来生成各种具体的类;而模板类指的是这些具体的、由模板生成的类,也就是vector<int>、vector<char>这些类。类似地,在下一节中我们要讲的函数模板(Function Template)也有对应的模板函数(Template Function)。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。