泛型编程
继承(公有、私有、保护)和包含,并不总能够满足重用代码的需要。
有一种概念,叫做容器(container class)。例如Stack
、Queue
。容器是用来设计存储各种对象和数据类型的。但之前提到的ADT中,我们把Stack
设计成只能存储unsigned long
值。如果我们想建立保存double
、或string
对象的stack
,那么要专门定义新的stack
。除了保存对象的类型不同,这两种stack
的其他代码应该是相同的。与其再编写一遍重复性很高的代码,不如编写一个泛型(generic)(意思是:独立于类型的)栈。对于泛型的栈,我们将具体的类型作为参数传递给这个类,这样就可以通过使用通用的代码,实现一个栈,能存储不同类型的值。
typedef
可以实现这种需求,然而,typedef有两个缺点:
- 每次修改类型都要编辑源代码。
- 在每个程序中只能用这种技术同时生成一种栈。即你不能让typedef同时代表两种不同的类型。
C++的类模板为生成通用的类声明提供了可靠的方法。模板提供参数化类型(parameterized),即能够将类型传递给接收方来建立类or函数。
泛型编程(Generic Programming)最初提出时的动机很简单直接:发明一种语言机制,能够帮助实现一个通用的标准容器库。所谓通用的标准容器库,就是要能够做到,比如用一个List类存放所有可能类型的对象这样的事;泛型编程让你编写完全一般化并可重复使用的算法,其效率与针对某特定数据类型而设计的算法相同。泛型即是指具有在多种数据类型上皆可操作的含义,与模板有些相似。STL巨大,而且可以扩充,它包含很多计算机基本算法和数据结构,而且将算法与数据结构完全分离,其中算法是泛型的,不与任何特定数据结构或对象类型系在一起。
类模板
定义类模板
这次的目标,就是写一个Stack模板类。定义模板时,以这样的代码开头:1
template<class Type>
关键字template
告诉编译器,将要定义一个模板。尖括号内的内容看做是一个参数列表。type
是类型名称。Type
并不一定是一个类,这里表明Type
是一个通用的类型说明符。较新的C++允许用不太容易混淆的关键字typename
来代替。
泛型标识符——这里的Type——叫做类型参数(type parameter),意味着他们也和变量一样,但赋给他们的是类型。当模板被调用时,Type将被具体的类型值(int、string、等等)所取代。用模板成员函数替代原有类的类方法,每个函数头都要以相同的模板声明打头。此外,需要将类限定符从Stack::
改成Stack<Type>::
。模板的具体使用被称为实例化(instantiation)或者具体化(specialization)。不能将模板函数放在独立实现的文件中。由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。
使用类模板
仅在程序包含模板时,不能生成模板类。必须请求实例化。需要声明一个类型为模板类的对象。方法是用所需的类型名替代泛型名。例如:1
2Stack<int> kernels;
Stack<string> colonels;Stack<int>
会用int替代模板中的所有Type,string也会如此做。
下面是Stack模板类的完整代码:
1 | //stack.h -- class defination for the Stack ADT |
深入讨论模板类
可以将内置类型或类作为模板Stack<T>
的类型。指针可以吗?比方说,不用string
对象,用C风格字符串处理方式——char *
作为参数。答案是:可以这么创建,但如果不对程序做重大修改,将无法很好的工作。接下来介绍原因,然后介绍一个指针栈的例子。
设计模板时应牢记的教训——切记盲目地使用模板。(你的代码,真的有对泛型的需求吗?)
这里用完全正确的Stack1
Stack<char *>st; //create a stack for pointers-to-char
版本一
1 | char * str; |
用char *
而不是string
对象来接受键盘输入。这种方法很快就失败了,因为仅仅创建了指针,没有为保存输入字符串而开辟空间。
版本二
1 | char str[40]; |
这里开辟了空间,str
的类型是char *
,也可以放在栈中。但在栈内的操作,会与我们假设的相冲突。
1 | template <class T> |
item
要必须引用某种类型的左值,而不是数组名。
版本三
1 | char *str = new char[40]; |
好似解决了上述问题,但又有新的问题:只有一个str
变量,而这个变量只会指向相同的内存单元。每当读取新的字符串时,内存的内容都发生改变,但执行压入操作的时候,加入到栈的地址都相同。因此,pop
时弹出的都是相同的地址,结果总是最后一个输入的字符串。总之,这样的栈没有任何用途。
正确的指针栈
提供一个指针数组,每个指针指向不同的字符串,把(指向不同内存空间的)指针放到栈中是有意义的。创建不同的指针,不是栈的职责,这点要搞清楚,栈是管理指针,而不是创建指针。
1 | template <class T> |
简单的数组模板
模板常用作容器类,因为类型参数的概念非常适合将相同的存储方案用于不同的类型。为容器提供可重用代码是引入模板的重要动机。下面是一个允许指定数组大小的简单数组模板。C++11新增模板array就是这么做的。
这里特别关注一下模板头:template<class T, int n>
关键字class
指明T
是类型参数,int
指出n
为一个int
型的参数。这种在模板头中出现的参数(特定类型而不是泛型名)称为非类型(non-type)或者表达式参数(expression)。
表达式参数有一些限制:
- 表达式参数可以是整型、枚举、指针、引用。因此
double n
是不合法的,但double *n
合法 - 模板代码不能修改表达式参数的参数值,不能使用表达式参数的地址。也就是说在ArrayTP中不能出现如
n++
或者&n
的表达式。 - 实例化模板时,用作表达式参数的值必须为常量表达式。
与Stack
中的构造函数相比:
stack
使用的动态分配管理的堆内存,表达式参数使用的是栈内存,执行速度快一点。表达式参数的主要缺点是,每种数组大小都将生成一个类。也就是说,下面的类会生成两个类声明;1
2ArrayTB<double, 12> a1;
ArrayTB<double, 13> a2;
而下面的两条声明只会有一个类声明1
2Stack<int> s1(12);
Stack<int> s2(13);
下面是简单的数组模板代码:
1 | #include<iostream> |
多功能的模板
模板能作为基类、组件类、作为其他模板的类型参数
模板类用作基类
1 | template <class T> |
递归使用模板
1 | ArrayTP< ArrayTP<int, 5> , 10> a; |
a是一个包含10个元素的数组,每个元素是一个包含5个int元素的数组
等价于 int a[10][5];
。模板语法中,维的顺序与等价的二位数组相反。
使用多个类型参数。希望模板可以保持两种(或多种)类型
1 | template<class T1, class T2> |
Pair
类可以保存两个不同的值。STL有类似的模板,名叫pair
默认类型模板参数
1 | template <class T1, class T2 = int> |
这样,如果在传参数时省略T2
,将默认使用int
1
2XXX<double,double>m1; //double double
XXX<double>m2; //double int
STL经常使用该特性。
不可以为函数模板提供参数默认值。可以为非类型参数提供默认值。
成员
模板可用作结构、类、模板类的成员。STL的实现必须使用这项特性
将模板作为参数
模板可包含类型参数(class T)、非类型参数(int n),模板还可以包含本身就是模板的参数。1
2
3
4
5
6
7
8template<template <class T> class Thing>
class Crab
{
private:
Thing<int> s1;
Thing<double> s2;
...
};template
里面有一个类型参数。这里的类型参数为template <class T> class Thing
。参数中template <class T> class
是类型,表示传进来的是一个模板类。Thing
是模板类名字。
例如如下的声明:1
Crab<King> legs;
这种声明要生效,模板参数King
必须是一个模板类。1
2template<class T>
class King ;
Crab
中的两个私有对象,由King<double>
和King<int>
替代。
C++代码重用总结
C++提供几种代码重用的手段。
代码重用机制是为了让程序员能够重用经过测试的代码,而不用手工复制它们。这样可以简化编程工作,提供程序的可靠性。
公有继承建立is-a
关系,派生类可以重用基类的代码。
私有继承和保护继承建立的是has-a
关系,基类的公有接口都将成为派生类的内部接口。这被称为继承,但不继承接口,因此派生类对象不能显示使用基类接口。因此不能像公有继承那样将派生类对象看成是一种基类对象。在不进行显示类型转换的情况下,基类指针或引用将不能指向派生类对象。
包含是一种比较常用的实现has-a
关系的手段,与私有继承和保护继承相比,更加容易使用。
私有继承和保护继承有一些不同的功能。例如继承允许派生类访问基类的保护成员。还允许派生类重新定义从基类那儿继承的虚函数。包含是不能使用这些功能的。
多重继承。私有MI和保护MI建立has-a
关系,公有MI建立is-a
关系。MI会带来一些问题:多次定义同一个名称、继承多个基类对象。可以用类限定符来解决名称二义性的问题,可以用虚基类来避免继承多个基类对象的问题。但使用虚基类后,要编写新的构造函数初始化列表以解决二义性问题。
类模板是一种典型的泛型编程例子。模板要以一句template打头,告诉编译器要定义一个模板。涉及到的知识点:类型参数、表达式参数、实例化、默认参数、模板做参数、成员……等等