《C++ Primer》第18章 18.3节习题答案

《C++ Primer》第18章 用于大型程序的工具

18.3节 多重继承与虚继承 习题答案

练习18.21:解释下列声明的含义,在它们当中存在错误吗?如果有,请指出来并说明错误的原因。

(a)class CADVehicle: public CAD, Vehicle{ ... };
(b)class DblList:public List, public List { ... };
(c)class iostream: public istream, public ostream { ... };

【出题思路】

理解多重继承的特性和规则。

【解答】

(b)错误,在一个派生列表中,同一基类只能出现一次,这里List出现了两次。

练习18.22:已知存在如下所示的类的继承体系,其中每个类都定义了一个默认构造函数:

class A { ... };
class B: public A { ... };
class C: public B { ... };
class X { ... };
class Y { ... };
class Z: public X, public Y { ... };
class MI: public C, public Z { ... };

对于下面的定义来说,构造函数的执行顺序是怎样的?

MI mi;

【出题思路】

了解多重继承中构造函数的构造顺序。

【解答】

构造函数的执行次序如下:

(1)A的构造函数

(2)B的构造函数

(3)C的构造函数

(4)X的构造函数

(5)Y的构造函数

(6)Z的构造函数

(7)MI的构造函数

练习18.23:使用练习18.22的继承体系以及下面定义的类D,同时假定每个类都定义了默认构造函数,请问下面的哪些类型转换是不被允许的?

class D: public X, public C { ... };
D *pd = new D;
(a) X *px = pd;			(b) A *pa = pd;
(c) B *pb = pd;			(d) C *pc = pd;

【出题思路】

了解多个基类情况下类型转换的特点。

【解答】

(b)和(c)是不允许的。因为C对B的继承是私有继承,使得在D中B的默认构造函数成为不可访问的,所以尽管存在从“D*”到“B*”以及从“D*”到“A*”的转换,但这些转换不可访问。

练习18.24:在第714页,我们使用一个指向Panda对象的Bear指针进行了一系列调用,假设我们使用的是一个指向Panda对象的ZooAnimal指针将发生什么情况,请对这些调用语句逐一进行说明。

【出题思路】

深入理解基于指针类型的查找和调用顺序。

【解答】

如果使用ZooAnimal指针,则只能使用ZooAnimal类中定义的操作。

pb->print(count);,通过基类指针调用虚函数,使用动态绑定,pb目前指向Panda对象,随意调用Panda::print(ostream&)。

pb->cuddle();,因为ZooAnimal类中没有定义cuddle操作,所以该调用出错。

pb->highlight();,因为ZooAnimal类中没有定义highlight操作,所以该调用出错。delete pb;,因为ZooAnimal类中定义了虚析构函数,所以Panda类中的析构函数也是虚函数,因此delete pb;通过虚机制调用Panda析构函数。随着Panda析构函数的执行,依次调用Endangered、Bear和ZooAnimal的析构函数。

所以,通过指向Panda对象的Bear指针或ZooAnimal指针进行上述调用,将以同样方式确定函数调用。

练习18.25:假设我们有两个基类Base1和Base2,它们各自定义了一个名为print的虚成员和一个虚析构函数。从这两个基类中我们派生出下面的类,它们都重新定义了print函数:

class D1:public Base1 { /* ... */ };
class D2:public Base2 { /* ... */ };
class MI:public D1, public D2 { /* ... */ };

通过下面的指针,指出在每个调用中分别使用了哪个函数:

Base1 *pb1 = new MI;
Base2 *pb2 = new MI;
D1 *pd1 = new MI;
D2 *pd2 = new MI;
(a)pb1->print();	(b)pd1->print();	(c)pd2->print();
(d)delete pb2;		(e)delete pd1;		(f)delete pd2;

【出题思路】

深入理解虚机制和指针类型调用的过程。

【解答】

(a)、(b)和(c)均通过基类指针调用虚函数print,这些基类指针当前都指向MI类对象,所以均调用MI::print();(d)、(e)和(f)均通过基类指针删除对象,这些基类指针当前都指向MI类对象,所以均通过虚机制调用MI析构函数,随着MI析构函数的执行,依次调用D2、Base2、D1和Base1的析构函数。

练习18.26:已知如上所示的继承体系,下面对print的调用为什么是错误的?适当修改MI,令其对print的调用可以编译通过并正确执行。

MI mi;
mi.print(42);

【出题思路】

了解通过类对象调用函数时需要注意的问题,并给出解决方案。

【解答】

因为mi.print(42);通过MI类对象调用print函数,编译器通过了名字查找,确定调用的是MI类中定义的print函数,但是MI类中定义的print函数需要std::vector<double>类型的参数,所以该调用是错误的。

改正:将MI中print的声明改为void print(int);,该print调用即可正确编译和执行。

练习18.27:已知如上所示的继承体系,同时假定为MI添加了一个名为foo的函数:

int ival;
double dval;
void MI::foo(double cval)
{
	int dval;
	//练习中的问题发生在此处
}
(a)列出在MI::foo中可见的所有名字。
(b)是否存在某个可见的名字是继承自多个基类的?
(c)将Base1的dval成员与Derived的dval成员求和后赋给dval的局部实例。
(d)将MI::dvec的最后一个元素的值赋给Base2::fval。
(e)将从Base1继承的cval赋给从Derived继承的sval的第一个字符。

【出题思路】

深入理解多重继承体系下类的作用域和可见域。

【解答】

(a)MI::foo中可见的所有名字有:ival,dval, cval, sval, fval, dvec和print
(b)Dval和print
(c)dval = Base1::dval + Derived::dval;
(d)fval = dvec.back()。
(e)sval[0] = cval。

练习18.28:已知存在如下的继承体系,在VMI类的内部哪些继承而来的成员无须前缀限定符就能直接访问?哪些必须有限定符才能访问?说明你的原因。

struct Base {
	void bar(int);//默认情况下是公有的
protected:
	int ival;
};
struct Derived1: virtual public Base {
	void bar(char);//默认情况是公有的
	void foo(char);
protected:
	char cval;
};
struct Derived2: virtual public Base {
	void foo(int);//默认情况下是公有的
protected:
	int ival;
	char cval;
};
class VMI: public Derived1, public Derived2 {   };

【出题思路】

了解在类继承中不同派生类的优先级顺序。

【解答】

从VMI类内部可以不加限定地访问继承成员bar和ival。bar在共享基类Base和派生类Derived1中都存在,但这只是在一条派生路径上,特定派生类实例的优先级高于共享基类实例,所以在VMI类内部不加限定地访问bar,访问到的是Derived1中的bar实例。ival在共享基类Base和派生类Derived2中都存在;同理,在VMI类内部不加限定地访问ival,访问到的是Derived2中的ival实例。

继承成员foo和cval需要限定:二者在Derived1和Derived2中都存在,Derived1和Derived2均为Base的派生类,访问优先级相同,所以,在VMI类内不加限定地访问foo和cval,会出现二义性。

练习18.29:已知有如下所示的类继承关系:

class Class { ... };
class Base: public Class { ... };
class D1: virtual public Base { ... };
class D2: virtual public Base { ... };
class MI: public D1, public D2 { ... };
class Final: public MI, public Class { ... };
(a)当作用于一个Final对象时,构造函数和析构函数的执行次序分别是什么?
(b)在一个Final对象中有几个Base部分?几个Class部分?
(c)下面的哪些赋值运算将造成编译错误?
Base *pb;		Class *pc;   MI *pmi;	D2 *pd2
(a)pb = new Class;		(b)pc = new Final;
(c)pmi = pb;			(d)pd2 = pmi;

【出题思路】

深入了解虚继承机制,析构函数和构造函数的执行顺序。

【解答】

(a)该类继承关系可表示如下:

 

如果定义Final对象,则创建该对象时按如下顺序调用构造函数:

Class();
Base();
D1();
D2();
MI();
Class();
Final();

注意,首先调用虚基类Base的构造函数(导致调用Class构造函数),然后按声明次序调用非虚基类的构造函数:首先是MI(),它导致调用D1()和D2(),然后是Class(),最后调用Final类的构造函数。

撤销Final对象时调用析构函数的次序与调用构造函数的次序相反,如下所示:

~Class();
~Base();
~D1();
~D2();
~MI();
~Class();
`Final();

(b)一个Final对象只有一个Base子对象,有两个Class子对象。

(c)错误有

bp = new Class;//不能用派生类指针指向基类对象
pc = new Final;//Class是Final的一个二义基类
pmi = pb;	   //不能用指向基类的指针指向派生类的指针进行赋值

练习18.30:在Base中定义一个默认构造函数、一个拷贝构造函数和一个接受int形参的构造函数。在每个派生类中分别定义这三种构造函数,每个构造函数应该使用它的实参初始化其Base部分。【出题思路】

本题练习派生类中不同类型构造函数的初始化方法。

【解答】

class Class {
	//...
};

class Base: public Class {
	//...
public:
	Base():ival(0) { }
	Base(int i): ival(i) { }
	Base(const Base& b):ival(b.ival) {  }
protected:
	int ival;
};

class D1: virtual public Base {
	// ...
public:
	D1(): Base(0) { };
	D1(int i) :Base(i) { };
	D1(const D1 &d): Base(d) {  }
};

class D2: virtual public Base {
	// ...
public:
	D2(): Base(0) { };
	D2(int i) :Base(i) { };
	D2(const D1 &d): Base(d) {  }
};

class MI: public D1, public D2 {
	// ...
public:
	MI(): Base(0) { };
	MI(int i) :Base(i), D1(i), D2(i) { };
	MI(const MI &m): Base(m), D1(m), D2(m) {  }
};

class Final: public MI, public Class {
	// ...
public:
	Final(): Base(0) { };
	Final(int i) :Base(i), MI(i) { };
	Final(const Final &f): Base(f), MI(f) {  }
};

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值