【和春笋一起学C++】(七十一)虚基类
前言在实际编程开发中使用虚基类的场景并不多主要用在多继承场景。因此把虚基类的学习作为一种拓展。目录1 虚基类产生的背景2 虚基类2.1 使用虚基类时的构造函数初始化问题2.2 使用虚基类时中间类同名方法问题1 虚基类产生的背景上篇文章《私有继承与保护继承》提到私有继承在编写代码时会一些产生一些二义性的问题如两个独立基类的同名方法该使用哪一个方法那C是怎么解决的呢答案是虚基类。下面举实例进行说明。假设有一个基类Worker从Worker派生出两个派生类Singer和Waiter由于Singer、Waiter和Worker是is-a关系所以使用的是公有派生。类声明如下#pragma once #include string using namespace std; 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 ~Worker() 0; virtual void Set(); virtual void show()const; }; class Waiter:public Worker { public: Waiter():Worker(), panache(0) {} Waiter(const string s, long n, int p 0) :Worker(s, n), panache(p) {} Waiter(const Worker wk, int p0) :Worker(wk), panache(p) {} void Set(); void show()const; private: int panache; }; class Singer :public Worker { public: Singer() :Worker(), voice(other) {} 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; protected: enum {other, alto, contralto, soprano, bass, baritone, tenor}; enum { Vtypes 7}; private: int voice; static char *pv[Vtypes]; };Singer类中定义了两个枚举变量用来表示声音的类型和数量。现在从Waiter类和Singer类公有派生出一个类SingerWaiter类如下class SingerWaiter:public Singer, public Waiter{...};由于Waiter和Singer都继承了一个Worker组件因此SingerWaiter将包含两个Worker组件。C的多态性指出可以将派生类对象的地址赋给基类指针如下SingerWaiter ed; Worker *pw ed;///二义性但第二条语句将出现二义性ed对象中包含两个Worker对象如图1所示。所以应使用类型转换来指定对象如下Worker* pw1 (Waiter*)ed; Worker* pw2 (Singer*)ed;图1但更重要的问题是SingerWaiter对象是一个人怎么能包含两个fullname和两个id呢这从逻辑上说不通。所以这里需要引入虚基类当C引入多重继承的同时它引入了一种新技术——虚基类(Virtual base class)使得多重继承成为可能。2 虚基类虚基类使得从多个类(拥有相同基类)派生出的对象只继承一个基类对象。通过在类声明中使用关键字virtual使Worker被用作Singer和Waiter的虚基类。如下class Singer :virtual public Worker class Waiter :public virtual Worker其中关键字virtual和public的顺序可以交换。当派生类使用关键字virtual来指示派生时基类就成为了虚基类。将Worker用作虚基类后SingerWaiter对象将只包含Worker对象的一个拷贝。从本质上说继承的Singer和Waiter对象共享一个Worker对象而不是各自引入Worker对象的拷贝。虚基类的继承关系如图2所示因为SingerWaiter对象只包含一个Worker子对象所以可以使用多态。图22.1 使用虚基类时的构造函数初始化问题使用虚基类时需要对类的构造函数初始化列表句法进行一些修改。原因如下假设使用SingerWaiter类的构造函数初始化列表句法进行初始化构造函数定义如下SingerWaiter(const Worker wk, int p 0, int v Singer::other):Waiter(wk, p),Singer(wk, v){}这个初始化列表句法存在什么问题呢将wk对象的值传递给基类构造函数时将通过两条不同的途径Singer构造函数和Waiter构造函数。为避免这种冲突C在基类是虚拟的时禁止信息通过中间类自动传递给基类。因此上述构造函数仅初始化成员panache和voicewk参数中的信息将不会传递给基类构造函数。不过编译器必须在构造派生类对象之前构造基类对象组件在上述情况下编译器将使用Worker的默认构造函数。如果不希望使用默认构造函数来构造虚基类对象则需要显式调用所需的基类构造函数。因此需要对上述初始化列表句法进行修改改成如下形式SingerWaiter(const Worker wk, int p 0, int v Singer::other):Worker(wk), Waiter(wk, p),Singer(wk, v){}上述代码将显式调用基类构造函数Worker(const Worker)。这里请注意对于虚基类来说必须显式调用基类构造函数但对于非虚基类来说这样做是非法的。如果类有间接虚基类除非只需使用该虚基类的默认构造函数否则必须显式调用该虚基类的某个构造函数。2.2 使用虚基类时中间类同名方法问题在Singer类和Waiter类中都有show函数和set函数假设在SingerWaiter类中没有对show函数和set函数进行重新定义现在定义一个SingerWaiter类对象并调用show函数如下SingerWaiter h1(Elise, 2005, 6, soprano); h1.show();///该语句将出现二义性对于单继承如果没有重新定义show()函数则将使用最近祖先中的定义。而在多继承中如果每个直接祖先都有一个show()函数则调用将出现二义性。上述语句中h1对象无法确定是调用Singer类中的show()函数还是Waiter类的show()函数。一种解决上述语句二义性问题的方式是使用作用域解析操作符如下SingerWaiter h1(Elise, 2005, 6, soprano); h1.Singer::show();///使用Singer类中的show()函数一种更好的方式是在SingerWaiter类中重新定义show()函数并指出要使用哪个show()函数。void SingerWaiter::show() { Singer::show(); }