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

用于大型程序的工具

--多重继承与虚继承

引言:

大多数应用程序使用单个基类公用继承,但是,在某些情况下,单继承是不够用的,因为可能无法为问题域建模,或者会对模型带来不必要的复杂性。

在这些情况下,多重继承可以更直接地为应用程序建模多重继承是从多于一个直接基类派生类的能力,多重继承的派生类继承其所有父类的属性

一、多重继承

1、定义多个类

为了支持多重继承,扩充派生列表:

class Bear : public ZooAnimal
{
    //...
};

以支持由逗号分隔的基类列表:

class Panda : public Bear,public Endangered
{
    //...
};

派生类为每个基类(显式或隐式地)指定了访问级别——public、protected 或 private。像单继承一样,只有在定义之后,类才可以用作多重继承的基类。对于类可以继承的基类的数目,没有语言强加的限制,但在一个给定派生列表中,一个基类只能出现一次。

2、多重继承的派生类从每个基类中继承状态
在多重继承下,派生类的对象包含每个基类的基类子对象:

    Panda ying_ying("ying_ying");

对象ying_yang包含一个 Bear类子对象(Bear类子对象本身包含一个ZooAnimal基类子对象)、一个Endangered类子对象以及Panda类中声明的非static数据成员(如果有的话)。

3、派生类构造函数初始化所有基类

构造派生类型的对象包含构造和初始化它的所有基类子对象。像继承单个基类的情况一样,派生类的构造函数可以在构造函数初始化式中给零个或多个基类传递值:

Panda::Panda(std::string name, bool onExhibit):
    Bear(name, onExhibit, "Panda"),
    Endangered(Endangered::critical) {}

Panda::Panda():
    Endangered(Endangered::critical) {}

4、构造次序

构造函数初始化式只能控制用于初始化基类的值,不能控制基类的构造次序。基类构造函数按照基类构造函数在类派生列表中的出现次序调用。对Panda而言,基类初始化的次序是:

1)ZooAnimal,从Panda的直接基类Bear沿层次向上的最终基类。

2)Bear,第一个直接基类

3)Endangered,第二个直接基类,它本身没有基类。

4)Panda,初始化Panda本身的成员,然后运行它的构造函数的函数体。

【注解】

构造函数调用次序既不受构造函数初始化列表中出现的基类的影响,也不受基类在构造函数初始化列表中出现的次序的影响。

例如,在Panda类的默认构造函数中,隐式调用Bear类的默认构造函数,它不出现在构造函数初始化列表中,但仍在显式列出的Endangered类构造函数之前调用Bear类的默认构造函数

5、析构的次序

总是按构造函数运行的逆序调用析构函数。如:

	~Panda, ~Endangered, ~Bear, ~ZooAnimal。

二、转换与多个基类

在单个基类情况下,派生类的指针或引用可以自动转换为基类的指针或引用,对于多重继承也是如此,派生类的指针或引用可以转换为其任意基类的指针或引用。

void print(const Bear &);
void highlight(const Endangered &);
ostream &operator<<(ostream &,const ZooAnimal &);

    Panda ying_ying("ying_ying");

    print(ying_ying);
    highlight(ying_ying);
    cout << ying_ying << endl;

在多重继承的情况下,遇到二义性转换的可能性更大。编译器不会试图根据派生类转换来区别基类间的转换,转换到每个基类都一样好:

void print(const Bear &);
void print(const Endangered &);

    Panda ying_ying("ying_ying");

    print(ying_ying);	//Error:ambiguous

//P617 习题17.25
class X
{
    //...
};
class A
{
    //...
};

class B : public A
{
    //...
};
class C : private B
{
    //...
};

class D : public X ,public C
{
    //...
};

    D *pd = new D;

    X *px = pd;
    B *pb = pd; //Error:注意C对B的继承是private!
    A *pa = pd; //Error
    C *pc = pd;

1、多重继承下的虚函数

假定我们的类定义了下表列出的虚成员:


ZooAnimal/Endangered类中的虚函数


函数


定义自己版本的类


print


ZooAnimal::ZooAnimal


Bear::Bear


Endangered::Endangered


Panda::Panda


highlight


Endangered::Endangered


Panda::Panda


toes


Bear::Bear


Panda::Panda


cuddle


Panda::Panda


析构函数


ZooAnimal::ZooAnimal


Endangered::Endangered

2、基于指针类型或引用类型的查找

像单继承一样,用基类的指针或引用只能访问基类中定义(或继承)的成员,不能访问派生类中引入的成员。

当一个类继承于多个基类的时候,那些基类之间没有隐含的关系,不允许使用一个基类的指针访问其他基类的成员。

Bear *pb = new Panda("ying_yang");
pb->print(cout);    // ok:参考上表
pb->cuddle();       // error
pb->highlight();    // error
delete pb;          // ok

在通过Endangered指针或引用访问Panda对象时,不能访问Panda接口的Panda特定的部分和Bear部分:

Endangered *pe = new Panda("ying_yang");
pe->print(cout);    // ok
pe->toes();         // error
pe->cuddle();       // error
pe->highlight();    // ok
delete pe;          // ok

3、确定使用哪个虚析构函数

假定所有根基类都将它们的析构函数适当定义为虚函数,那么,无论通过哪种指针类型删除对象,虚析构函数的处理都是一致的:

//每个指针都指向Panda对象
delete pz;
delete pb;
delete pp;
delete pe;

假定这些指针每个都向Panda对象,则每种情况下发生完全相同的析构函数调用次序。析构函数调用的次序是构造函数次序的逆序:通过虚机制调用Panda析构函数。随着Panda析构函数的执行,依次调用Endangered、Bear和ZooAnimal的析构函数。

三、多重继承派生类的复制控制

多重继承的派生类的逐个成员初始化、赋值和析构,表现得与单继承下的一样,使用基类自己的复制构造函数、赋值操作符或析构函数隐式构造、赋值或撤销每个基类。

假定Panda类使用默认复制控制成员。ling_ling的初始化

	 Panda ying_yang("ying_yang");
	 Panda ling_ling = ying_yang;

使用默认复制构造函数调用Bear复制构造函数,Bear复制构造函数依次在执行 Bear复制构造函数之前运行ZooAnimal复制构造函数。一旦构造了ling_ling的 Bear部分,就运行Endangered复制构造函数来创建对象的那个部分。最后,运行Panda复制构造函数。

合成的赋值操作符的行为类似于复制构造函数。

合成的析构函数撤销Panda对象的每个成员,并且按构造次序的逆序为基类部分调用析构函数。

【小心地雷】

像单继承的情况一样,如果具有多个基类的类定义了自己的析构函数,该析构函数只负责清除派生类。如果派生类定义了自己的复制构造函数或赋值操作符,则类负责复制(赋值)所有的基类子部分。只有派生类使用复制构造函数或赋值操作符的合成版本,才自动复制或赋值基类部分。

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

时间: 2024-12-16 03:03:54

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

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

用于大型程序的工具 --多重继承与虚继承[续1] 四.多重继承下的类作用域 成员函数中使用的名字和查找首先在函数本身进行,如果不能在本地找到名字,就继续在本类中查找,然后依次查找每个基类.在多重继承下,查找同时检察所有的基类继承子树 -- 在我们的例子中,并行查找 Endangered子树和Bear/ZooAnimal子树.如果在多个子树中找到该名字,则那个名字的使用必须显式指定使用哪个基类;否则,该名字的使用是二义性的. [小心地雷] 当一个类有多个基类的时候,通过对所有直接基类同时进行名字查

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

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

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.能够处理更复杂的应用概念[多重