3. 多重继承
上节Student同时继承了String和Valarray类,这个就是多重继承了。这个功能很强大,因为跟实际相符(比如现在的平板,有时可以看出一个电脑,有时也可以归为手机),但用起来也很麻烦,一个很简单的问题就是假如两个(多个)基类都有同名的方法怎么办。
书中举了一个抽象基类Worker,派生出Waiter和Singer两个基类,然后可以定义一个SingerWaiter类同时继承Waiter和Singer
先把Worker,Waiter,Singer三个类的声明抄过来:
class Worker
{
private:
string fullname;
long id;
public:
Worker() : fullname("no one"), id(0L) {}
Worker(const string & s, long n) : fullname(s), id(n){}
virtual ~ Wokrer() = 0;
virtual void Set();
Virtual voirdShow() const;
};
class Waiter : public Worker
{
private:
int panache;
public:
Waiter(): Worker(), pannache(0) {}
Waiter(const string & s, long n, int p = 0): Worker(s,n), panache(p) {}
Waiter(const Worker & wk, int p = 0) : Worker(wk), panache(p) {}
void Set();
void Show() const;
} ;
class Singer : public Worker
{
protected:
enum{other, alto, contralto, soprano, bass, baritone, tenor};
enum{Vtypes = 7};
private:
static char * pv[Vtypes];
int voice;
public:
Singer(): Worker(), pannache(0) {}
Singer(const string & s, long n, int v = other): Worker(s,n), voice(v) {}
Singer(const Worker & wk, int v = other) : Worker(wk), voice(v) {}
void Set();
void Show() const;
}
Set就是给数据成员赋值的接口,Show就是显示数据成员的接口。这三个类还是很好理解的。
注意基类的指针可以指向派生类,所以书中可以定义一个即包含Waiter对象又包含Singer对象的数组:
Worker * pw[4] = {&Waiter_object1, &Singer_object1,&Waiter_object2, &Singer_object2}
这里重点说SingerWaiter这个类。声明很简单:
class SingerWaiter: public Singer, public Waiter{...}
首先假设SingerWaiter这个类已经定义好了,这个时候如果声明一个SingerWaiter类的对象ed,还有基类Worker的指针指向它就会出现二义性:
SingerWaiter ed; Worker * pw = &ed;
这个时候应该显示指示Worker指针怎么指向ed:
Worker * pw1 = (Waiter *) & ed;
Worker * pw1 = (Singer*) & ed;
其次要考虑是Waiter从哪个类继承Worker类的fullname和id两个数据成员,这涉及一种新技术:虚基类
虚基类似的从多个类(它们的基类相同)派生出的对象只继承一个基类对象,比如这里可以使Worker被用作Singer和Waiter的虚基类:
class Singer : virtual public Worker { ...};
class Waiter : public virtual Worker { ...};
(public和virtual)之间的顺序无所谓。
然后可以定义SingerWaiter:
class SingerWaiter: public Singer, public Waiter { ...};
这样SingerWaiter就只包含Worker对象的一个副本,即只包含一组fullname和id两个数据成员
这只是第一步。使用虚基类之后,需要对类构造函数采用一种新的方法。如果有一个普通继承关系:C类继承B类,B类继承A类,那么C类的构造函数只能调用B类的构造函数,并将初始值传给B类的构造函数,B类的构造函数只能调用C类的构造函数,同时将初始值传给A类的构造函数。但如果使用虚基类,比如这里的SingerWaiter类,需要也可以直接调用基类Worker的构造函数,这是为了避免WaiterSinger与Waiter和Singer类共有的数据不知道怎么传给基类Worker,同时基类Waiter和Singer也必须得初始化
SingerWaiter(const Worker & wk, int p = 0, int v = other) : Worker(wk), Waiter(wk,p), Singer(wk, v) {}
如果这几个类都共有一个方法,比如说Show(),这个时候最好显示定义SingerWaiter类的show(),就算SingerWaiter的show()和Singer或者Waiter的show()一样。如果没有定义的话,必须显示声明调用Waiter还是Singer的Show():
SingerWaiter newhire(); newhire.Singer::Show();
总之,在祖先相同时,使用多重继承必须引入虚基类,并修改构造函数初始化列表的规则。如果编写这些基类没有考虑到多重继承,必须得修改。
有了虚基类继承,非虚基类继承,肯定也有虚基类非虚基类混合继承,然后就复杂多了,编程时必须时刻注意。当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分布表示各条非虚途径的多个基类子对象。
4. 类模板
前面讲过stack和queue类,理想的方法是这个类又可以放int数据,又可以放double数据,或者其他数据。这个时候就需要类模板,前面提到的valarray就是这样一个类
类模板在类声明之前添加 template <class Type> 表示这是一个类模板;跟模板函数一样,类模板的方法定义前也需要添加template <class Type>,还需要在类后面添加<type>
比如stack的构造函数:
template <class Type>
Stack<Type>::Stack () {...}
使用的时候必须请求实例化,为此需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名:
Stack<int> kernels 这里声明一个只能放int数据的stack类对象kernerls
类模板与函数模板很相似,因为可以有隐式实例化,显示实例化和显示具体化。
隐式实例化就是上面的例子,对应的显示实例化就是在前面显示写上template class。
显示具体化是特定类型的定义,比如stack要放一个string类,可能需要某种专门针对string类的方法,在这个时候就需要显示具体化了。具体化类模板定义格式如下:
template <> class Classname<specialized-type-name> {...}
模板类可用作其他类,结构和模板的成员,模板类也可以有友元,具体规则就很多了,这里就不一一详写了