《C++ Primer》第18章 用于大型程序的工具
本章介绍了一些用于大型程序开发的语言特性,包括:
●异常处理。
●命名空间。
●多重继承与虚继承。
本章的练习着重帮助读者理解异常的基本概念,练习异常捕获和异常处理的设计;理解命名空间和名字解析;理解多重继承和虚继承的概念,并进行相应的设计练习。
18.1节习题答案
练习18.1:在下列throw语句中异常对象的类型是什么?
(a) range_error r("error"); (b) exception *p = &r;
throw r; throw *p;
如果将(b)中的throw语句写成了throw p将发生什么情况?
【出题思路】
理解异常对象的类型的匹配方法和规则。
【解答】
(a)异常对象r的类型是range_error。
(b)被抛出的异常对象是对指针p解引用的结果,其类型与p的静态类型相匹配,为exception。若写成throw p,则抛出的异常对象是exception*类型。读者可尝试编译、运行这几条语句,观察系统的提示。
练习18.2:当在指定的位置发生了异常时将出现什么情况?
void exercise(int *b, int *e)
{
vector<int> v(b,e);
int *p = new int[v.size()];
ifstream in("ints");
//此处发生异常
}
【出题思路】
深入理解异常发生时可能对程序已运行部分产生的影响。
【解答】
在new操作后发生的异常使得动态分配的数组没有被撤销,从而造成内存泄漏。
练习18.3:要想让上面的代码在发生异常时能正常工作,有两种解决方案。请描述这两种方法并实现它们。
【出题思路】
在理解异常发生可能造成的后果之后,尝试使用try-catch方法和封装策略正确处理异常。
【解答】
方法一:将有可能发生异常的代码放在try块中,以便在异常发生时捕获异常。
void exercise(int *b, int *e)
{
vector<int> v(b, e);
int *p = new int[v.size()];
try{
ifstream in("ints");
//此处发生异常
}
catch {
delete p; //释放数组
//进行其他处理
}
// ...
}
方法二:定义一个类来封装数组的分配和释放,以保证正确释放资源:
class Resource {
public:
Resource(size_t sz): r(new int[sz]) { }
~Resource() { if(r) delete r; }
private:
int *r;
}
函数exercise相应修改为:
void exercise(int *b, int *e)
{
vector<int> v(b, e);
int *p = new int[v.size()];
Resource res(v.size());
ifstream in("ints");
//此处发生异常
// ...
}
注意,此处给出的Resource类非常简略,实际应用时,还需定义其他操作,包括复制构造函数、复制操作、解引用操作、箭头操作、下标操作等,以支持内置指针及数组的使用方式并保证自动删除Resource对象所引用的数组。另外,可将该Resource类定义为类模板,以支持多种数组元素类型。
练习18.4:查看图18.1(第693页)所示的继承体系,说明下面的try块有何错误并修改它。
try {
//使用C++标准库
} catch(exception) {
// ...
} catch(const runtime_error &re) {
// ...
} catch(overflow_error eobj) { /* ... */ }
【出题思路】
理解异常处理中catch语句的匹配顺序。
【解答】
该try块中使用的exception、runtime_error及overflow_error是标准库中定义的异常类。它们是因继承而相关的:runtime_error类继承exception类,overflow_error类继承runtime_error类。在使用来自继承层次的异常时,catch子句应该从最低派生类型到最高派生类型排序,以便派生类型的处理代码出现在其基类类型的catch之前,所以上述块中,catch子句的顺序错误。
可修改为:
try {
//使用C++标准库
} catch(overflow_error eobj) {
// ...
} catch(const runtime_error &re) {
// ...
} catch(exception) { /* ... */ }
练习18.5:修改下面的main函数,使其能捕获图18.1(第693页)所示的任何异常类型:
int main(){
//使用C++标准库
}
处理代码应该首先打印异常相关的错误信息,然后调用abort(定义在cstdlib头文件中)终止main函数。
【出题思路】
本题练习捕获并打印所有类型的异常。
【解答】
int main(){
try {
//使用C++标准库
} catch(const exception &e) {
cerr << e.what() << endl;
abort();
}
return 0;
}
练习18.6:已知下面的异常类型和catch语句,书写一个throw表达式使其创建的异常对象能被这些catch语句捕获:
(a)class exceptionType { };
catch(exceptionType *pet) { }
(b)catch(...) { }
(c)typedef int EXCPTYPE;
catch(EXCPTYPE) { }
【出题思路】
理解使用throw时异常对象的类型一般需要与catch语句捕获的对象类型对应。
【解答】
(a)throw new exceptionType(); //动态创建一个异常对象并抛出其指针
(b)throw 8; //被抛出的表达式为任意类型
(c)throw 11; //被抛出的表达式为int类型即可
练习18.7:根据第16章的介绍定义你自己的Blob和BlobPtr,注意将构造函数写成函数try语句块。【出题思路】
本题练习在实际应用中处理构造函数异常的常用方法:使用try语句块。
【解答】
template <typename T> class Blob {
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
//构造函数
Blob();
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); }
// Blob中的元素数目
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
//添加和删除元素
void push_back(const T &t) {data->push_back(t); }
//移动版本,参见13.6.3节(第548页)
void push_back(T &&t) { data->push_back(std::move(t)); }
void pop_back();
//元素访问
T& back();
T& operator[](size_type i); //在14.5节中定义
private:
std::shared_ptr<std::vector<T>> data;
//若data[i]无效,则抛出msg
void check(size_type i, const std::string &msg) const;
};
template <typename T> class BlobPtr {
public:
BlobPtr(): curr(0) { }
BlobPtr(Blob<T> &a, size_t sz = 0) try:
wptr(a.data), curr(sz) { }
} catch(const std::bad_alloc &e) { handle_out_of_memory(e); }
T& operator*() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr];//(*p)为本对象指向的vector
}
//递增和递减
BlobPtr& operator++(); //前置运算符
BlobPtr& operator--();
private:
//若检查成功,check返回一个指向vector的shared_ptr
std::shared_ptr<std::vector<T>>
check(std::size_t, const std::string &) const;
//保存一个weak_ptr,表示底层vector可能被销毁
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr;//数组中的当前位置
}
练习18.8:回顾你之前编写的各个类,为它们的构造函数和析构函数添加正确的异常说明。如果你认为某个析构函数可能抛出异常,尝试修改代码使得该析构函数不会抛出异常。
【出题思路】
练习18.7中已经说明了构造函数的异常处理方法,练习18.8中着重理解析构函数的异常说明。
【解答】
class MyTest_Base
{
public:
virtual ~MyTest_Base()
{
cout << "开始准备销毁一个MyTest_Base类型的对象" << endl;
//把异常完全封装在构构函数内部
try {
//注意:在析构函数中抛出了异常
throw std::exception("在析构函数中故意抛出一个异常,测试!");
}
catch(...) { }
}
void Func() throw()
{
throw std::exception("故意抛出一个异常,测试!");
}
void Other() { }
}//重点是把可能发生异常的部分用try封装起来
练习18.9:定义本节描述的书店程序异常类,然后为Sales_data类重新编写一个复合赋值运算符并令其抛出一个异常。
【出题思路】
本题练习书店应用程序中复合赋值运算符的编写和异常判断。
【解答】
Sales_data& Sales_data::operator-=(const Sales_data &rhs)
{
if(isbn() != rhs.isbn())
throw isbn_mismatch("wrong isbns", isbn(), rhs.isbn());
units_sold -= rhs.unist_sold;
revenue -= rhs.revenue;
return *this;
}
练习18.10:编写程序令其对两个ISBN编号不相同的对象执行Sales_data的加法运算。为该程序编写两个不同的版本:一个处理异常、另一个不处理异常。观察并比较这两个程序的行为,用心体会当出现了一个未被捕获的异常时程序会发生什么情况。
【出题思路】
体会处理或不处理异常对程序的影响。下面是处理异常和不处理异常的主要程序片段。
【解答】
//处理异常
Sales_data item1, item2, sum;
while(cin >> item1 >> item2) { //读取两条交易信息
try {
sum = item1 + item2; //计算它们的和
return sum;
}catch(const isbn_mismatch &e) {
cerr << e.what() << ": left isbn(" << e.left
<< ") right isbn(" << e.right << ")" << endl;
}
}
//不处理异常
Sales_data item1, item2, sum;
while(cin >> item1 >> item2) {//读取两条交易信息
sum = item1 + item2; //计算它们的和
return sum;
}
练习18.11:为什么what函数不应该抛出异常?
【出题思路】
深入理解what函数的作用和在异常处理中所处的重要位置。
【解答】
what函数是在catch异常后用于提取异常基本信息的虚函数,what函数是确保不会抛出任何异常的。如果what函数抛出了异常,则会在新产生的异常中由于what函数继续产生异常,将会产生抛出异常的死循环。所以what函数必须确保不抛出异常