用于大型程序的工具
--多重继承与虚继承[续1]
四、多重继承下的类作用域
成员函数中使用的名字和查找首先在函数本身进行,如果不能在本地找到名字,就继续在本类中查找,然后依次查找每个基类。在多重继承下,查找同时检察所有的基类继承子树 —— 在我们的例子中,并行查找 Endangered子树和Bear/ZooAnimal子树。如果在多个子树中找到该名字,则那个名字的使用必须显式指定使用哪个基类;否则,该名字的使用是二义性的。
【小心地雷】
当一个类有多个基类的时候,通过对所有直接基类同时进行名字查找。多重继承的派生类有可能从两个或多个基类继承同名成员,对该成员不加限定的使用是二义性的。
1、多个基类可能导致二义性
假定Bear类和 Endangered类都定义了名为print的成员,如果Panda类没有定义该成员,则
ying_yang.print(cout);
这样的语句将导致编译时错误。
如果每个print调用明确指出想要哪个版本 —— Bear::print还是Endangered::print,也可以避免错误。只有在存在使用该成员的二义性尝试的时候,才会出错。如果只在一个基类子树中找到声明,则标识符得以确定而查找算法结束。例如:
ying_yang.population();
可以通过编译,名字population将只在基类Endangered中找到,并且在Bear类或其任意基类中都不会出现。
2、首先发生名字查找
[着重理解这一段!]虽然两个继承的print成员的二义性相当明显,但是也许更令人惊讶的是:
1,即使两个继承的函数有不同的形参表,也会产生错误。
2,即使函数在一个类中是私有的而在另一个类中是公用或受保护的,也是错误的。
3,如果在ZooAnimal类中定义了print而 Bear类中没有定义,调用仍是错误的。
[释疑]名字查找总是以两个步骤发生:首先编译器找到一个匹配的声明(或者,在这个例子中,找到两个匹配的声明,这导致二义性),然后,编译器才确定所找到的声明是否合法。
3、避免用户级二义性
可以通过指定使用哪个类解决二义性:
ying_yang.Endangered::print(cout);
避免潜在二义性最好的方法是,在解决二义性的派生类中定义函数的一个版本。例如,应该给选择使用哪个print版本的 Panda类一个 print函数:
std::ostream& Panda::print(std::ostream &os) const { Bear::print(os); Endangered::print(os); return os; }
//P621 习题17.29/30/31/32 struct Base1 { void print(int) const; protected: int ival; double dval; char cval; protected: int *id; }; struct Base2 { void print(double) const; protected: double fval; private: double dval; }; struct Derived : public Base1 { void print(std::string ) const; protected: std::string sval; double dval; }; struct MI : public Derived,public Base2 { void print(std::vector<double>); void bar() { int sval; Derived::dval = 3.14; fval = 0; cval = ‘a‘; sval = *ival; *id = 1; } void fooBar(double cval) { int dval; dval = Base1::dval + Derived::dval; Base2::fval = *(dvec.end() - 1); *(sval.begin()) = Base1::cval; } protected: int *ival; std::vector<double> dvec; }; int main() { MI mi; mi.Derived::Base1::print(42); }
五、虚继承
【实例】每个 IO库类都继承了一个共同的抽象基类,那个抽象基类管理流的条件状态并保存流所读写的缓冲区。istream和 ostream类直接继承这个公共基类,库定义了另一个名为iostream的类,它同时继承istream和ostream,iostream类既可以对流进行读又可以对流进行写。
多重继承的类从它的每个父类继承状态和动作,如果IO类 型使用常规继承,则每个iostream对象可能包含两个ios子对象:一个包含在它的istream子对象中,另一个包含在它的ostream子对象中,从设计角度讲,这个实现正是错误的:iostream类想要对单个缓冲区进行读和写,它希望跨越输入和输出操作符共享条件状态。如果有两个单独的ios对象,这种共享是不可能的。
在C++中,通过使用虚继承解决这类问题。虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类。
istream和 ostream类对它们的基类进行虚继承。通过使基类成为虚基类,istream 和ostream指定,如果其他类(如iostream)同时继承它们两个,则派生类中只出现它们的公共基类的一个副本。通过在派生列表中包含关键字virtual设置虚基类:
class istream : public virtual ios { //... }; class ostream : public virtual ios { //... }; class iostream : public istream,public ostream { //... };
一个不同的Panda类
在动物学圈子中,对于Panda是属于 Raccoon科还是 Bear科已经争论了100年以上。因为软件设计主要是一种服务行业,所以最现实的解决方案是从二者派生Panda:
class Panda : public Bear,public Raccoon,public Endangered { //... };
虚继承Panda层次如图所示:
虚继承有一个不直观的特征:必须在提出虚派生的任意实际需要之前进行虚派生。只有在使用Panda的声明时,虚继承才是必要的,但如果 Bear类和 Raccoon类不是虚派生的,Panda类的设计者就没有好运气了。
实际上,中间基类指定其继承为虚继承的要求很少引起任何问题。通常,使用虚继承的类层次是一次性由一个人或一个项目设计组设计的,独立开发的类很少需要其基类中的一个是虚基类,而且新基类的开发者不能改变已经存在的层次。
六、虚基类声明
通过用关键字virtual修改声明,将基类指定为通过虚继承派生:
class Raccoon : public virtual ZooAnimal { //... }; class Bear : virtual public ZooAnimal { //... };
指定虚派生只影响从指定了虚基类的类派生的类。除了影响派生类自己的对象之外,它也是关于派生类与自己的派生类的关系的一个陈述。
任何可被指定为基类的类也可以被指定为虚基类,虚基类可以包含通常由非虚基类支持的任意类元素。
1、支持到基类的常规转换
即使基类是虚基类,也照常可以通过基类类型的指针或引用操纵派生类的对象:
void dance(const Bear *) { cout << "dance" << endl; } void rummage(const Raccoon *) { cout << "rummage" << endl; } ostream &operator<<(ostream &,const ZooAnimal &) { cout << "operator <<" << endl; } Panda ying_ying; dance(&ying_ying); rummage(&ying_ying); cout << ying_ying << endl;
2、虚基类成员的可见性
使用虚基类的多重继承层次比没有虚继承的引起更少的二义性问题。
【说明:】
可以无二义性地直接访问共享虚基类中的成员。同样,如果只沿一个派生路径重定义来自虚基类的成员,则可以直接访问该重定义成员。在非虚派生情况下,两种访问都可能是二义性的。
假定通过多个派生路径继承名为X的成员,有下面三种可能性:
1.如果在每个路径中X表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例。
2.如果在某个路径中X是虚基类的成员,而在另一路径中X是后代派生类的成员,也没有二义性 —— 特定派生类实例的优先级高于共享虚基类实例。
3.如果沿每个继承路径X表示后代派生类的不同成员,则该成员的直接访问是二义性的。
像非虚多重继承层次一样,这种二义性最好用在派生类中提供覆盖实例的类来解决。
//P625 习题17.33 class Base { public: int bar(int); protected: int ival; }; class Derived1 : virtual public Base { public: int bar(int); int foo(char); protected: char cval; }; class Derived2 : virtual public Base { public: int foo(int); protected: int ival; char cval; }; class VMI : public Derived1,public Derived2 { void test() { ival; bar(10); Base::bar(10); Derived1::cval; Derived2::cval; Derived1::foo(‘a‘); Derived2::foo(‘b‘); } };
C++ Primer 学习笔记_96_用于大型程序的工具 --多重继承与虚继承[续1],布布扣,bubuko.com