第十八章 多重继承与虚继承
第十八章 用于大型程序的工具
大规模应用程序的特殊要求包括:
- 在独立开发的子系统之间协同处理错误的能力。
- 使用各种库进行协同开发的能力。
- 对比较复杂的应用概念建模的能力。
异常处理
异常处理(exception handling)机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并作出相应的处理。
抛出异常
在C++语言中,我们通过抛出(throwing)一条表达式来引发(raised)一个异常。异常类型和当前的调用链决定了哪段处理代码(handler)将用来处理该异常。
程序的控制权从throw
转移到catch
模块。
栈展开:当throw
出现在一个try语句块
时,检查该try语句块
相关的catch
字句,若有匹配则处理;若无匹配,则继续检查外层的try
匹配的catch
。
若一个异常没有被捕获,则它将终止当前的程序。
对象销毁:
- 块退出后,它的局部对象将被销毁。
- 若异常发生在构造函数中,即使某个对象只构造了一部分,也要确保已构造的成员正确地被销毁。
- 将资源释放放在类的析构函数中,以保证资源能被正确释放。析构函数本身不会引发异常。
练习18.1
在下列
throw
语句中异常对象的类型是什么?
(a) range_error r("error"); throw r; (b) exception *p = &r; throw *p;
解:
- (a):
range_error
- (b):
exception
练习18.2
当在指定的位置发生了异常时将出现什么情况?
void exercise(int *b, int *e) { vector<int> v(b, e); int *p = new int[v.size()]; ifstream in("ints"); //此处发生异常 }
解:
指针p
指向的内容不会被释放,将造成内存泄漏。
练习18.3
要想让上面的代码在发生异常时能正常工作,有两种解决方案。请描述这两种方法并实现它们。
解:
方法一:不使用指针,使用对象:
struct intArray { intArray() : p(nullptr) { } explicit intArray(std::size_t s): p(new int[s]) { } ~intArray() { delete[] p; } // data meber int *p; }; intArray p(v.size());
方法二:使用智能指针:
std::shared_ptr<int> p(new int[v.size()], [](int *p) { delete[] p; });
捕获异常
若无需访问抛出的异常对象,则可以忽略捕获形参的名字。
通常,若catch
接受的异常与某个继承体系有关,则最好将该catch
的参数定义成引用类型。
搜索catch
未必是最佳匹配,而是第一个匹配,因此,越细化的catch
越应该放在catch
列表前段。
重新抛出:catch
代码执行一条throw;
将异常传递给另一个catch
语句。
捕获所有异常:catch(...)
练习18.4
查看图18.1所示的继承体系,说明下面的
try
块有何错误并修改它。
try { // 使用 C++ 标准库 } catch (exception) { // ... } catch (const runtime_error &re) { // ... } catch (overflow_error eobj) { /* ... */ }
解:
细化的异常类型应该写在前面:
try { // 使用 C++ 标准库 } catch (overflow_error eobj) { // ... } catch (const runtime_error &re) { // ... } catch (exception) { /* ... */ }
练习18.5
修改下面的
main
函数,使其能捕获图18.1所示的任何异常类型:
int main(){ // 使用 C++标准库 }
处理代码应该首先打印异常相关的错误信息,然后调用 abort
终止函数。
解:
略
练习18.6
已知下面的异常类型和
catch
语句,书写一个throw
表达式使其创建的异常对象能被这些catch
语句捕获:
(a) class exceptionType { }; catch(exceptionType *pet) { } (b) catch(...) { } (c) typedef int EXCPTYPE; catch(EXCPTYPE) { }
解:
(a): throw exceptionType(); (b): throw expection(); (c): EXCPTYPE e = 1; throw e;
构造函数
处理构造函数初始值异常的唯一方法是将构造函数协程函数try
语句块。
示例:
template <typename T> Blob<T>::Blob(std::initializer_list<T> il) try: data(std::make_shared<std::vector<T> >(il){ /*函数体*/ } catch(const std::bad_alloc &e){ handle_out_of_memory(e); }
练习18.7
根据第16章的介绍定义你自己的
Blob
和BlobPtr
,注意将构造函数写成函数try
语句块。
解:
略
noexcept异常说明
使用noexcept
说明指定某个函数不会抛出异常。
示例:
void recoup(int) noexcept; //C++11 coid recoup(int) throw(); //老版本
练习18.8
回顾你之前编写的各个类,为它们的构造函数和析构函数添加正确的异常说明。如果你认为某个析构函数可能抛出异常,尝试修改代码使得该析构函数不会抛出异常。
解:
略
异常类层次
标准exception层次:
- exception
- bad_cast
- bad_alloc
- runtime_error
- overflow_error
- underflow_error
- range_error
- logic_error
- domain_error
- invalid_argument
- out_of_range
- length_error
自定义异常类:
示例:
class out_of_stock: public std::runtime_error { explicit out_of_stock(const std::string &s): std::runtime_error(s){ } };
练习18.9
定义本节描述的书店程序异常类,然后为
Sales_data
类重新编写一个复合赋值运算符并令其抛出一个异常。
练习18.10
编写程序令其对两个
ISBN
编号不相同的对象执行Sales_data
的加法运算。为该程序编写两个不同的版本:一个处理异常,另一个不处理异常。观察并比较这两个程序的行为,用心体会当出现了一个未被捕获的异常时程序会发生什么情况。
解:
略
练习18.11
为什么
what
函数不应该抛出异常?
解:
略
练习18.12
将你为之前各章练习编写的程序放置在各自的命名空间中。也就是说,命名空间chapter15包含
Query
程序的代码,命名空间chapter10包含TextQuery
的代码;使用这种结构重新编译Query
代码实例。
解:
略
练习18.13
什么时候应该使用未命名的命名空间?
解:
需要定义一系列静态的变量的时候。
参考:https://stackoverflow.com/questions/154469/unnamed-anonymous-namespaces-vs-static-functions
练习18.14
假设下面的
operator*
声明的是嵌套的命名空间mathLib::MatrixLib
的一个成员:
namespace mathLib { namespace MatrixLib { class matrix { /* ... */ }; matrix operator* (const matrix &, const matrix &); // ... } }
请问你应该如何在全局作用域中声明该运算符?
解:
mathLib::MatrixLib::matrix mathLib::MatrixLib::operator* (const mathLib::MatrixLib::matrix &, const mathLib::MatrixLib::matrix &);
练习18.15
说明
using
指示与using
声明的区别。
解:
- 一条
using
声明语句一次只引入命名空间的一个成员。 using
指示使得某个特定的命名空间中所有的名字都可见。
有点像python中的import
:
from lib import func from lib import *
练习18.16
假定在下面的代码中标记为“位置1”的地方是对命名空间 Exercise 中所有成员的
using
声明,请解释代码的含义。如果这些using
声明出现在“位置2”又会怎样呢?将using
声明变为using
指示,重新回答之前的问题。
namespace Exercise { int ivar = 0; double dvar = 0; const int limit = 1000; } int ivar = 0; //位置1 void main() { //位置2 double dvar = 3.1416; int iobj = limit + 1; ++ivar; ++::ivar; }
解:
略
练习18.17
实际编写代码检验你对上一题的回答是否正确。
解:
略
练习18.18
已知有下面的
swap
的典型定义,当mem1
是一个string
时程序使用swap
的哪个版本?如果mem1
是int
呢?说明在这两种情况下名字查找的过程。
void swap(T v1, T v2) { using std::swap; swap(v1.mem1, v2.mem1); //交换类型的其他成员 }
解:
std::swap
是一个模板函数,如果是string
会找到string
版本;反之如果是int
会找到int
版本。
练习18.19
如果对
swap
的调用形如std::swap(v1.mem1, v2.mem1)
将会发生什么情况?
解:
会直接调用std
版的swap
,但对后面的调用无影响。
命名空间
多个库将名字放置在全局命名空间中将引发命名空间污染(namespace pollution)。命名空间(namespace)分割了全局命名空间,其中每个命名空间是一个作用域。
命名空间定义
命名空间的定义包含两部分:1.关键字namespace
;2.命名空间名称。后面是一系列由花括号括起来的声明和定义。命名空间作用域后面无需分号。
示例:
namespace cplusplus_primer{ }
每个命名空间都是一个作用域。定义在某个命名空间内的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌套作用域中的任何单位访问。位于该命名空间之外的代码必须明确指出所用的名字是属于哪个命名空间的。
命名空间可以是不连续的。这点不同于其他作用域,意味着同一命名空间可以在多处出现。
内联命名空间(C++11):
无需使用该命名空间的前缀,通过外层命名空间就可以直接访问。
示例:
namespace cplusplus_primer{ inline namespace FifthEd{ // 表示本书第5版代码 class Query_base {}; } } cplusplus_primer::Query_base qb;
未命名的命名空间:
指关键字namespace
后面紧跟花括号的用法。未命名的命名空间中定义的变量拥有静态的声明周期:在第一次使用前创建,直到程序结束才销毁。不能跨越多个文件。
使用命名空间成员
像namespace_name::member_name
这样使用命名空间的成员非常繁琐。
命名空间的别名:
namespace primer = cplusplus_primer;
using声明(using declaration):
一条using
声明语句一次只引入命名空间的一个成员。
using std::string; string s = "hello";
using指示(using directive):
使得某个特定的命名空间中所有的名字都可见。
using namespace std; string s = "hello";
类、命名空间与作用域
namespace A{ class C1{ public: int f3(); } } A::C1::f3
重载与命名空间
using
声明语句声明的是一个名字,而非特定的函数,也就是包括该函数的所有版本,都被引入到当前作用域中。
练习18.20
在下面的代码中,确定哪个函数与
compute
调用匹配。列出所有候选函数和可行函数,对于每个可行函数的实参与形参的匹配过程来说,发生了哪种类型转换?
namespace primerLib { void compute(); void compute(const void *); } using primerLib::compute; void compute(int); void compute(double, double = 3.4); void compute(char*, char* = 0); void f() { compute(0); }
解:
略
多重继承与虚继承
多重继承
练习18.21
解释下列声明的含义,在它们当作存在错误吗?如果有,请指出来并说明错误的原因。
(a) class CADVehicle : public CAD, Vehicle { ... }; (b) class DbiList : public List, public List { ... }; (c) class iostream : public istream, public ostream { ... };
练习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;
类型转换与多个基类
练习18.23
使用练习18.22的继承体系以及下面定义的类
D
,同时假定每个类都定义了默认构造函数,请问下面的哪些类型转换是不被允许的?
class D : public X, public C { ... }; p *pd = new D; (a) X *px = pd; (b) A *pa = pd; (c) B *pb = pd; (d) C *pc = pd;
练习18.24
在第714页,我们使用一个指向
Panda
对象的Bear
指针进行了一系列调用,假设我们使用的是一个指向Panda
对象的ZooAnimal
指针将会发生什么情况,请对这些调用语句逐一进行说明。
练习18.25
假设我们有两个基类
Base1
和Base2
,它们各自定义了一个名为
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;
struct Base1 { void print(int) const; protected: int ival; double dval; char cval; private: 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>); protected: int *ival; std::vector<double> dvec; };
多重继承下的类作用域
- 当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况。此时,不加前缀限定符直接使用该名字将引发二义性。
练习18.26
已知如上所示的继承体系,下面对
MI
,令其对
MI mi; mi.print(42);
练习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的第一个字符。
虚继承
- 虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象成为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
- 虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。
练习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 { };
构造函数与虚继承
- h含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最底层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序对其进行初始化。
- 虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。
练习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;
练习18.30
在
Base
中定义一个默认构造函数、一个拷贝构造函数和一个接受int
形参的构造函数。在每个派生类中分别定义这三种构造函数,每个构造函数应该使用它的形参初始化其Base
部分。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)