# Chapter 18 Tools for Large Programs

大规模应用程序的特殊要求包括:

  • 在独立开发的子系统之间协同处理错误的能力。
  • 使用各种库进行协同开发的能力。
  • 对比较复杂的应用概念建模的能力。

# Exception Handling


# 异常处理

异常处理(exception handling)机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并作出相应的处理。

# 抛出异常

在 C++ 语言中,我们通过抛出(throwing)一条表达式来引发(raised)一个异常。异常类型和当前的调用链决定了哪段处理代码(handler)将用来处理该异常。

程序的控制权从 throw 转移到 catch 模块。

栈展开:当 throw 出现在一个 try语句块 时,检查该 try语句块 相关的 catch 字句,若有匹配则处理;若无匹配,则继续检查外层的 try 匹配的 catch

若一个异常没有被捕获,则它将终止当前的程序。

对象销毁:

  • 块退出后,它的局部对象将被销毁。
  • 若异常发生在构造函数中,即使某个对象只构造了一部分,也要确保已构造的成员正确地被销毁。
  • 将资源释放放在类的析构函数中,以保证资源能被正确释放。析构函数本身不会引发异常。

# 捕获异常

若无需访问抛出的异常对象,则可以忽略捕获形参的名字。

通常,若 catch 接受的异常与某个继承体系有关,则最好将该 catch 的参数定义成引用类型。

搜索 catch 未必是最佳匹配,而是第一个匹配,因此,越细化的 catch 越应该放在 catch 列表前段。

重新抛出: catch 代码执行一条 throw; 将异常传递给另一个 catch 语句。

捕获所有异常: catch(...)

# 构造函数

处理构造函数初始值异常的唯一方法是将构造函数协程函数 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); }

# noexcept 异常说明

使用 noexcept 说明指定某个函数不会抛出异常。

示例:

void recoup(int) noexcept; //C++11
coid recoup(int) throw(); // 老版本

# 异常类层次

标准 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){ }
};

# Exercise 18.1

在下列 throw 语句中异常对象的类型是什么?

(a) range_error r("error");
	throw r;
(b) exception *p = &r;
	throw *p;

解:

  • (a): range_error
  • (b): exception

# Exercise 18.2

当在指定的位置发生了异常时将出现什么情况?

void exercise(int *b, int *e)
{
	vector<int> v(b, e);
	int *p = new int[v.size()];
	ifstream in("ints");
	// 此处发生异常
}

解:

指针 p 指向的内容不会被释放,将造成内存泄漏。

# Exercise 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; });

# Exercise 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) { /* ... */ }

# Exercise 18.5

修改下面的 main 函数,使其能捕获图 18.1 所示的任何异常类型:

int main(){
	// 使用 C++ 标准库
}

处理代码应该首先打印异常相关的错误信息,然后调用 abort 终止函数。

解:

# Exercise 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;

# Exercise 18.7

根据第 16 章的介绍定义你自己的 BlobBlobPtr ,注意将构造函数写成函数 try 语句块。

解:

# Exercise 18.8

回顾你之前编写的各个类,为它们的构造函数和析构函数添加正确的异常说明。如果你认为某个析构函数可能抛出异常,尝试修改代码使得该析构函数不会抛出异常。

解:

# Exercise 18.9

定义本节描述的书店程序异常类,然后为 Sales_data 类重新编写一个复合赋值运算符并令其抛出一个异常。

# Exercise 18.10

编写程序令其对两个 ISBN 编号不相同的对象执行 Sales_data 的加法运算。为该程序编写两个不同的版本:一个处理异常,另一个不处理异常。观察并比较这两个程序的行为,用心体会当出现了一个未被捕获的异常时程序会发生什么情况。

解:

# Exercise 18.11

为什么 what 函数不应该抛出异常?

解:

# Namespaces


# 命名空间

多个库将名字放置在全局命名空间中将引发命名空间污染(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 声明语句声明的是一个名字,而非特定的函数,也就是包括该函数的所有版本,都被引入到当前作用域中。


# Exercise 18.12

将你为之前各章练习编写的程序放置在各自的命名空间中。也就是说,命名空间 chapter15 包含 Query 程序的代码,命名空间 chapter10 包含 TextQuery 的代码;使用这种结构重新编译 Query 代码实例。

解:

# Exercise 18.13

什么时候应该使用未命名的命名空间?

解:

需要定义一系列静态的变量的时候。

参考:https://stackoverflow.com/questions/154469/unnamed-anonymous-namespaces-vs-static-functions

# Exercise 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 &);

# Exercise 18.15

说明 using 指示与 using 声明的区别。

解:

  • 一条 using 声明语句一次只引入命名空间的一个成员。
  • using 指示使得某个特定的命名空间中所有的名字都可见。

有点像 python 中的 import :

from lib import func
from lib import *

# Exercise 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;
}

解:

# Exercise 18.17

实际编写代码检验你对上一题的回答是否正确。

解:

# Exercise 18.18

已知有下面的 swap 的典型定义,当 mem1 是一个 string 时程序使用 swap 的哪个版本?如果 mem1int 呢?说明在这两种情况下名字查找的过程。

void swap(T v1, T v2)
{
	using std::swap;
	swap(v1.mem1, v2.mem1);
	// 交换类型的其他成员
}

解:

std::swap 是一个模板函数,如果是 string 会找到 string 版本;反之如果是 int 会找到 int 版本。

# Exercise 18.19

如果对 swap 的调用形如 std::swap(v1.mem1, v2.mem1) 将会发生什么情况?

解:

会直接调用 std 版的 swap ,但对后面的调用无影响。

# Exercise 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);
}

解:

# Multiple and Virtual Inheritance


# 多重继承与虚继承

# 多重继承

# 类型转换与多个基类

# 多重继承下的类作用域

  • 当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况。此时,不加前缀限定符直接使用该名字将引发二义性。

# 虚继承

  • 虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象成为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
  • 虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。

# 构造函数与虚继承

  • h 含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最底层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序对其进行初始化。
  • 虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。

# Exercise 18.21

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

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

# Exercise 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;

# Exercise 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;

# Exercise 18.24

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

# Exercise 18.25

假设我们有两个基类 Base1Base2 ,它们各自定义了一个名为 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;
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;
};

# Exercise 18.26

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

MI mi;
mi.print(42);

# Exercise 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的第一个字符。

# Exercise 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 { };

# Exercise 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;

# Exercise 18.30

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

# Chapter Summary

🍓:)