C++ Primer 学习笔记_96_用于大型程序的工具 --多重继承与虚继承[续1]

用于大型程序的工具

--多重继承与虚继承[续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

时间: 2024-10-21 02:44:27

C++ Primer 学习笔记_96_用于大型程序的工具 --多重继承与虚继承[续1]的相关文章

C++ Primer 学习笔记_97_用于大型程序的工具 --多重继承与虚继承[续2]

用于大型程序的工具 --多重继承与虚继承[续2] 七.特殊的初始化语义 从具有虚基类的类继承的类对初始化进行特殊处理:在虚基类中,由最低层派生类的构造函数初始化虚基类.在ZooAnimal示例中,使用常规规则将导致Bear 类和 Raccoon类都试图初始化Panda对象的ZooAnimal类部分. 虽然由最低层派生类初始化虚基类,但是任何直接或间接继承虚基类的类一般也必须为该基类提供自己的初始化式.只要可以创建虚基类派生类类型的独立对象,该类就必须初始化自己的虚基类,这些初始化只在创建中间类型

C++ Primer 学习笔记_95_用于大型程序的工具 --多重继承与虚继承

用于大型程序的工具 --多重继承与虚继承 引言: 大多数应用程序使用单个基类的公用继承,但是,在某些情况下,单继承是不够用的,因为可能无法为问题域建模,或者会对模型带来不必要的复杂性. 在这些情况下,多重继承可以更直接地为应用程序建模.多重继承是从多于一个直接基类派生类的能力,多重继承的派生类继承其所有父类的属性. 一.多重继承 1.定义多个类 为了支持多重继承,扩充派生列表: class Bear : public ZooAnimal { //... }; 以支持由逗号分隔的基类列表: cla

C++ Primer 学习笔记_89_用于大型程序的工具 --异常处理[续2]

用于大型程序的工具 --异常处理[续2] 八.自动资源释放 考虑下面函数: void f() { vector<string> v; string s; while (cin >> s) { v.push_back(s); } string *p = new string[v.size()]; //... delete p; } 在正常情况下,数组和vector都在退出函数之前被撤销,函数中最后一个语句释放数组,在函数结束时自动撤销vector. 但是,如果在函数内部发生异常,则将

C++ Primer 学习笔记_90_用于大型程序的工具 --异常处理[续3]

用于大型程序的工具 --异常处理[续3] 九.auto_ptr类[接上] 5.auto_ptr对象的复制和赋值是破坏性操作 auto_ptr和内置指针对待复制和赋值有非常关键的区别.当复制auto_ptr对象或者将它的值赋给其他auto_ptr对象的时候,将基础对象的所有权从原来的auto_ptr对象转给副本,原来的auto_ptr对象重置为未绑定状态. auto_ptr<string> strPtr1(new string("HELLO!")); auto_ptr<

C++ Primer 学习笔记_93_用于大型程序的工具 --命名空间[续2]

用于大型程序的工具 --命名空间[续2] 五.类.命名空间和作用域 名字的可见性穿过任意嵌套作用域,直到引入名字的块的末尾. 对命名空间内部使用的名字的查找遵循常规C++查找规则:当查找名字的时候,通过外围作用域外查找.对命名空间内部使用的名字而言,外围作用域可能是一个或多个嵌套的命名空间,最终以全包围的全局命名空间结束.只考虑已经在使用点之前声明的名字,而该使用仍在开放的块中: namespace A { int i; namespace B { int i; int j; int f1()

C++ Primer 学习笔记_94_用于大型程序的工具 --命名空间[续3]

用于大型程序的工具 --命名空间[续3] 六.重载与命名空间 正如我们所见,每个命名空间维持自己的作用域,因此,作为两个不同命名空间的成员的函数不能互相重载.但是,给定命名空间可以包含一组重载函数成员. 1.候选函数与命名空间 命名空间对函数匹配有两个影响.一个影响是明显的:using声明或using 指示可以将函数加到候选集合.另一个影响则微妙得多. 正如前节所见,有一个或多个类类型形参的函数的名字查找包括定义每个形参类型的命名空间.这个规则还影响怎样确定候选集合,为找候选函数而查找定义形参类

C++ Primer 学习笔记_91_用于大型程序的工具 --命名空间

用于大型程序的工具 --命名空间 引言: 在一个给定作用域中定义的每个名字在该作用域中必须是唯一的,对庞大.复杂的应用程序而言,这个要求可能难以满足.这样的应用程序的全局作用域中一般有许多名字定义.由独立开发的库构成的复杂程序更有可能遇到名字冲突 -- 同样的名字既可能在我们自己的代码中使用,也可能(更常见地)在独立供应商提供的代码中使用. 库倾向于定义许多全局名字 -- 主要是模板名.类型名或函数名.在使用来自多个供应商的库编写应用程序的时候,这些名字中有一些几乎不可避免地会发生冲突,这种名字

C++ Primer 学习笔记_92_用于大型程序的工具 --命名空间[续1]

用于大型程序的工具 --命名空间[续1] 二.嵌套命名空间 一个嵌套命名空间即是一个嵌套作用域 -- 其作用域嵌套在包含它的命名空间内部.嵌套命名空间中的名字遵循常规规则:外围命名空间中声明的名字被嵌套命名空间中同一名字的声明所屏蔽.嵌套命名空间内部定义的名字局部于该命名空间.外围命名空间之外的代码只能通过限定名引用嵌套命名空间中的名字. 嵌套命名空间可以改进库中代码的组织: namespace cplusplus_primer { namespace QueryLib { class Quer

C++ Primer 学习笔记_87_用于大型程序的工具 --异常处理

用于大型程序的工具 --异常处理 引言: C++语言包括的一些特征在问题比較复杂,非个人所能管理时最为实用.如:异常处理.命名空间和多重继承. 相对于小的程序猿团队所能开发的系统需求而言,大规模编程[往往涉及数千万行代码]对程序设计语言的要求更高.大规模应用程序往往具有下列特殊要求: 1.更严格的正常运转时间以及更健壮的错误检測和错误处理.错误处理常常必须跨越独立开发的多个子系统进行[异常处理]. 2.能够用各种库(可能包括独立开发的库)构造程序[命名空间]. 3.能够处理更复杂的应用概念[多重