C++ Primer(第五版)|练习题答案与解析(第十八章:用于大型程序的工具)

C++ Primer(第五版)|练习题答案与解析(第十八章:用于大型程序的工具)

本博客主要记录C++ Primer(第五版)中的练习题答案与解析。
参考:C++ Primer
C++Primer
C++Primer

练习题18.1

在下列throw语句中异常对象的类型是什么?
(a)range_error r(“error”); throw r;
(b) exception *p = &r; throw *p;
如果将(b)中的thorw语句写成throw p会发生什么?

(a)中的异常对象的类型是range_error,它用于报告内部计算中的范围错误。
(b)中的异常对象的类型是exception。抛出表达式的静态编译时类型决定了异常对象的类型。
如果(b)中的“thorw”被写成“throw p”,就会出现运行时错误。

练习题18.2

当指定位置发生了异常时将出现什么情况?
void exercise(int *b, int *e)
{
vector<int> v(b, e);
int *p = new int[v.size()];
ifstream in(“ints”);
// 此处发生异常
}

发生异常,所在块之前的临时变量会被销毁,v会调用vector类(标准库,P686)的析构函数进行销毁,并释放相关内存,P指针会被销毁,但是P指针指向的内存由于是动态分配的,所以该内存不会被释放,造成内存泄漏。输入流对象会调用ifstream类的析构函数销毁,最后程序被终止。标准库类型能确保它们的析构函数不会引发异常。

练习题18.3

要想让上面的代码在发生异常时能正常工作,有两种解决方案。请描述这两种方法并实现它们。

因为上述代码只有指针P发生了内存泄漏(异常),所以需要解决的问题是在发生异常时,自动释放其指向的内存。

  • 方法1:使用智能指针,并传入删除的lambda表达式:shared_ptr<int> p(new int[v.size()], [](int *p) { delete[] p; });//lambda表达式相当于一个删除器
  • 方法2:创建一个包含一个int*的类,在析构函数进行delete文件指针。
class intAr  
{  
    int *p=nullptr;  
public:  
    intAr(size_t n):p(new int[n]){}  
    ~intAr()  
    {  
        delete[]p;  
    }  
}  

练习题18.4

查看图18.1(P693)所示的继承体系,说明下面的try块有何错误并修改它。
try {
//使用C++标准库
}
catch (exception) {
//…
}
catch (const runtime_error &re) {
//…
}
catch(overflow_error eobj){/**/}

P687
搜索匹配的catch语句过程中,找到的未必是最佳匹配,是第一个可以匹配的catch语句,所以越是专门、越是特例化的catch语句应该放在前面,因为catch语句是按照其出现顺序逐一匹配的。
若所个catch语句的类型之间存在这继承关系,应该将继承链的最低端的类放在前面。
所以应该将继承最低端的类放在最前面,顺序倒过来即可。

练习题18.5

修改下面的main函数。使其能捕获图18.1(P693)所示的任何异常类型:
int main(){
//使用C++标准库
}
处理代码应该首先打印异常相关的错误信息,然后调用abort(定义在cstdlib头文件中)终止main函数。

P173,每个标准库异常类都包含了一个名为what的成员函数,这个函数没有参数,返回值是C风格的字符串(const char *)。

#include <iostream>  
#include <cstdlib>  
int main()  
{  
    using namespace std;  
    try {  
        //  
    }  
    catch (overflow_error e)  
    {  
        cout << e.what();  
        abort();  
    }  
    catch (underflow_error u)  
    {  
        cout << u.what();  
        abort();  
    }  
    catch (range_error r)  
    {  
        cout << r.what();  
        abort();  
    }  
    catch (domain_error d)  
    {  
        cout << d.what();  
        abort();  
    }  
    catch (invalid_argument i)  
    {  
        cout << i.what();  
        abort();  
    }  
    catch (out_of_range o)  
    {  
        cout << o.what();  
        abort();  
    }  
    catch (length_error l)  
    {  
        cout << l.what();  
        abort();  
    }  
    catch (runtime_error r)  
    {  
        cout << r.what();  
        abort();  
    }  
    catch (logic_error l)  
    {  
        cout << l.what();  
        abort();  
    }  
    catch (bad_alloc b)  
    {  
        cout << b.what();  
        abort();  
    }  
    catch (bad_alloc b)  
    {  
        cout << b.what();  
        abort();  
    }  
    catch (exception e)  
    {  
        cout << e.what();  
        abort();  
    }  
    return 0;  
} 

练习题18.6

已知下面的异常类型和catch语句,书写一个throw表达式使其创建的异常对象能被这些catch语句捕获:
(a) class exceptionTYpe { }; catch(exceptionType *pet) { }
(b) catch(…) { }
© typedef int EXCPTYPE; catch(EXCPTYPE) { }

(a )throw &exceptionType()
(b )任何异常,P688。
(c ) throw int()

练习题18.7

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

P690,在构造函数上加上try即可。

template<typename T>  
Blob<T>::Blob() try:data(std::make_shared<std::vector<T>>()) {  
}  
catch (const std::bad_alloc &e) {  
    std::cerr << e.what() << std::endl;  
}  

练习题18.9

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

主要需要添加isbn_mismatch,一个继承的异常类,可以看书。

练习题18.11

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

what函数是发生异常后用来表示异常的具体信息的。
因为它是noexcept ,所以它不能够throw 出异常,当它内部调用的方法抛出异常会直接调用std::terminate()结束当前程序,而不会被catch到。

练习题18.13

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

P700,希望所定义的对象、函数、类类型或其他实体,只在程序的一小段代码中可见,这样可以进一步的缓解名字空间的冲突。
P701,根据C++11标准,static定义静态变量的做法已取消,现在是定义一个全局的未命名的名字空间。在未命名的名字空间中定义的变量都是静态的。

练习题18.14

假设下面的operator声明的是嵌套的命名空间mathLib::MatrixLib的一个成员:
namespace mathLib {
namespace MatrixLib {
class matrix{/
/};
matrix operator

(const matrix &, const matrix &);
}
}

注意每一个变量都需要加作用域限定符。

mathLib::MatrixLib::matrix mathLib::MatrixLib::operator*(matrix &a,matrix &b);

练习题18.15

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

P702,using声明语句依次只引入命名空间的一个成员。using指示无法控制哪个名字是可见的,因为所有名字都是可见的。

练习题18.16

假定下面的代码中标记为“位置1”的地方是对于命名空间Exercise中所有成员using的声明,请解释代码的含义。如果这些using声明出现在“位置2”又会怎样呢?将using变为using指示,重新回答之前的问题。

练习题18.17

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

测试1.1

namespace Exerciese {
	int ivar = 0;
	double dvar = 0;
	const int limit = 1000;
}
int ivar = 0;
//位置1
using Exerciese::ivar;//错误,与全局变量ivar冲突,多次声明  
using Exerciese::dvar;
using Exerciese::limit;
void manip()  
{	
	//位置2
	double dvar = 3.1416;//覆盖using声明的dvar  
	int iobj = limit + 1;
	++ivar;  
	++::ivar;  
}  

测试1.2

namespace Exerciese {
	int ivar = 0;
	double dvar = 0;
	const int limit = 1000;
}
int ivar = 0;
//位置1

void manip()  
{	
	//位置2
	using Exerciese::ivar;//隐藏全局变量   
	using Exerciese::dvar;
	using Exerciese::limit;
	double dvar = 3.1416;//错误,多重定义,多次初始化,当前dvar对象已经可见   
	int iobj = limit + 1;
	++ivar;  //Exerciese的ivar
	++::ivar; //全局变量 
}  

测试2.1

namespace Exerciese {
	int ivar = 0;
	double dvar = 0;
	const int limit = 1000;
}
int ivar = 0;
//位置1
using namespace Exerciese;
void manip()  
{	
	//位置2
	double dvar = 3.1416;//覆盖using声明的dvar  
	int iobj = limit + 1;
	++ivar;//错误,不明确,二义性,二者都可见  
	++::ivar;  
}  

测试2.2

namespace Exerciese {
	int ivar = 0;
	double dvar = 0;
	const int limit = 1000;
}
int ivar = 0;
//位置1
void manip()  
{	
	//位置2
	using namespace Exerciese;
	double dvar = 3.1416;//覆盖using声明的dvar  
	int iobj = limit + 1;
	++ivar;//错误,不明确,二义性,二者都可见   
	++::ivar;  
}  

练习题18.18

已知有下面对的swap的典型定义(参见13.3节,第P457),当meml是一个string时程序使用swap的哪个版本?如果meml是int呢?说明在这两种情况下的名字查找的过程。
void swap(T v1, T v2) {
using std::swap;
swap(v1.mem, v2.mem);
//交换类型T的其他成员
}

当参数为string时,会先在string类中查找swap函数,找到则不使用std版本的。若为 int类型,则直接使用标准库版本的swap。

练习题18.19

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

只能使用标准库版本的swap。

练习题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);
}
如果将using声明置于f函数compute的调用点之前将发生什么情况?

namespace primerLib  
{  
    void compute();//不可行  
    void compute(const void *);//可行,0->NULL  
}  
using primerLib::compute;  
void compute(int);//可行,最佳匹配  
void compute(double, double = 1.1);//可行,int->double  
void compute(char*, char* = 0);//可行,0->NULL  
void f()  
{  
    compute(0);//与compute(int)版本最佳匹配  
}  
namespace primerLib{  
    void compute();//不可行,可见  
    void compute(const void *);//可行,0->NULL,可见  
}  
void compute(int);//可行,不可见,被隐藏
void compute(double, double = 1.1);//可行,int->double,被隐藏 
void compute(char*, char* = 0);//可行,0->NULL,被隐藏
void f(){  
    using primerLib::compute;  
    compute(0);
}  

练习题18.21

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

(a)class CADVehicle : public CAD, Vehicle{};
CADVehicle公开继承自CAD,私有继承Vehicle。CADVehicle能获取Vehicle的所有公共和私有方法,但不能转换为Vehicle的参数。这是“无法访问的”基础。比如:

CadVehicle example;
void foo(Vehicle){/*do something*/};
foo(CADVehicle);//will not work, will work if Vehicle were public

(b)class DBList: public List,public List {/*do something*/};
错误,因为试图两次从相同的基类派生。如果两个不同的库或头文件定义了同一个命名类,需要使用范围解析操作符来指定,比如headerfile_name::List
©class iostream : public istream, public ostream{/*do something*/};正确。

练习题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 {};
class D : public X, public C{};
对于下面的定义来说,构造函数的执行顺序是怎样的?
MI mi;

基类构造的顺序取决于它们在类派生列表中出现的顺序。构造如下:A、B、C、X、Y、Z、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;

可以令某个可访问基类的指针或引用直接指向一个派生类对象,但该指针只能访问其对应的基类部分或者基类的基类部分。所以所有的转换都是允许的。

练习题18.24

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

ZooAnimal *pb = new Panda ("ying_yang");
pb->print();//正确, 属于 ZooAnimal 接口
pb->cuddle();//错误, 不是接口的一部分
pb->highlight();//错误, 不是接口的一部分
delete pb;//正确, 属于接口的一部分

练习题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 *pb1 = new MI;
D2 *pd2 = new MI;
(a ) pb1->print(); (b ) pd1->print(); (c ) pd2->print();
(d ) delete pb2; (e ) delete pd1; (f )delere pd2;

#include <iostream>
struct Base1
{
	virtual void print() { std::cout << "Print from Base1" << std::endl; }
	virtual ~Base1() { std::cout << "Base1" << std::endl; }
};
struct Base2
{
	virtual void print() { std::cout << "Print from Base2" << std::endl; }
	virtual ~Base2() { std::cout << "Base2" << std::endl; }
};

struct D1 : public Base1
{
	void print() override { std::cout << "Print from D1" << std::endl; }
	~D1() override { std::cout << "D1" << std::endl; }
};
struct D2 : public Base2
{
	void print() override { std::cout << "Print from D2" << std::endl; }
	~D2() override { std::cout << "D2" << std::endl; }
};
struct MI : public D1, public D2
{
	void print() override { std::cout << "Print from MI" << std::endl; }
	~MI() override { std::cout << "MI" << std::endl; }
};
int main()
{
	Base1 *pb1 = new MI;
	Base2 *pb2 = new MI;
	D1 *pd1 = new MI;
	D2 *pd2 = new MI;
	std::cout << "pb1 print..........." << std::endl;
	pb1->print();
	std::cout << "pd1 print..........." << std::endl;
	pd1->print();
	std::cout << "pd2 print..........." << std::endl;
	pd2->print();
	std::cout << "delete pb2..........." << std::endl;
	delete pb2;
	std::cout << "delete pd1..........." << std::endl;
	delete pd1;
	std::cout << "delete pd2..........." << std::endl;
	delete pd2;
}

测试:

pb1 print...........
Print from MI
pd1 print...........
Print from MI
pd2 print...........
Print from MI
delete pb2...........
MI
D2
Base2
D1
Base1
delete pd1...........
MI
D2
Base2
D1
Base1
delete pd2...........
MI
D2
Base2
D1
Base1

练习题18.26

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

#include <iostream>
#include <vector>
struct Base1 {
	void print(int) const {
		std::cout << "Base1 Print Used" << std::endl;
	};
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>) {};
	void print(int x) {
		Base1::print(x);
	}
protected:
	int *ival;
	std::vector<double> dvec;
};
using namespace std;
int main()
{
	MI mi;
	mi.print(42);
	return 0;
}

测试:Base1 Print Used
MI中没有匹配整数参数的print版本。如果只是删除MI中的print函数,那么print的派生版本和Base2版本之间就会产生歧义。因此,应该重载print()的MI版本来获取一个int参数。

练习题18.27

已知如上所示的继承体系,同时假设为MI添加一个名为foo的函数:
int ival;
double dval;
void MI::foo(double foo){
int dval;
//练习中的问题发生在此处
}
(a)列出在MI::foo中可见的名字
(b)是否存在某个可见的名字是继承多个基类型?
(c)将Base1的dval成员与Derived的dval成员求和后赋值给dval的局部实例
(d)将MI::dvec的最后一个愿随的值赋给Base2::fval。
(e)将从Base1继承的cval赋给Derived继承的sval的第一个字符。

#include <iostream>
#include <vector>
struct Base1 {
	void print(int) const {
		std::cout << "Base1 Print Used" << std::endl;
	};
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 = std::string(1, Base1::cval);//(e)
	double dval;
};
struct MI : public Derived, public Base2 {

	void print(std::vector<double>) {};
	void print(int x) {
		Base1::print(x);
	}

	int ival;
	double dval;

	void foo(double cval)
	{
		int dval;
		dval = Base1::dval + Derived::dval;//(c)
		Base2::fval = dvec.back() - 1;//(d)
		Derived::sval[0] = Base1::cval;//(e)
		std::cout << dval;
	}
protected:
	std::vector<double> dvec = { 9,8,7 };
};
int main()
{
	MI mi;
	mi.foo(1.5);
	return 0;
}

(a)MI派生的类的所有属性都是可见的,私有的类除外。
(b)是的,基类中任何重复且非私有的名称都可以通过添加作用域操作符在foo中访问。
(c)、(d)、(e)如上所示。

练习题18.28

已知存在如下的继承体系,在VMI类的内部哪些继承而来的成员无须前缀限定符就能直接访问?哪些必须有限定符才能访问?

虚继承的目的是令某个类做出声明,承诺愿意共享它的基类,共享的基类子对象称为及虚基类,在此情况下,无论虚基类在集成体系中出现多少次,派生类中都只包含唯一一个共享的虚基类对象

struct Base{
    void bar(int); //默认情况下是公有的。没有限定访问,没有使用int定义 arg anywhere
protected:
    int ival;//需要限定,VMI将默认使用Derived2::ival
};
struct Derived1 : virtual public Base{
    void bar(char);//无条件访问, VMI派生于Derived1,后者派生于Base。
    void foo(char);//需要加限定符, 可以在两个foos之间转换参数。
protected:
    char cval;//需要加限定符,防止与其他cval产生二义性。
};

struct Derived2 : virtual public Base{
void foo(int);//需要加限定符, 可以在两个foos之间转换参数。
protected:
    int ival;//没有限制访问。
    char cval;//需要加限定符,防止与其他cval产生二义性。
};
class VMI : public Derived1, public Derived2 { };

练习题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 )下面哪些赋值运算会造成编译错误?

(a)P721,编译器按基类的声明顺序对其依次进行检查,以确定其中是否含有虚基类。如果有,则先构造虚基类,然后按声明的顺序逐一构造其他非虚基类。对象的销毁顺序与其构造顺序正好相反。构造顺序:Class、Base、D1、D2、MI、Class、FInal。析构顺序正好相反。
(b)1个Base部分,2个Class部分。
(c)

#include <iostream>
using namespace std;
class Class {
public:
	Class() { cout << "Class() called" << endl; }
	~Class() { cout << "~Class() called" << endl; }
};
class Base : public Class {
public:
	Base() { cout << "Base() called" << endl; }
	~Base() { cout << "~Base() called" << endl; }
};
class D1 : virtual public Base {
public:
	D1() { cout << "D1() called" << endl; }
	~D1() { cout << "~D1() called" << endl; }
};
class D2 : virtual public Base {
public:
	D2() { cout << "D2() called" << endl; }
	~D2() { cout << "~D2() called" << endl; }
};
class MI : public D1, public D2 {
public:
	MI() { cout << "MI() called" << endl; }
	~MI() { cout << "~MI() called" << endl; }
};
class Final : public MI, public Class {
public:
	Final() { cout << "Final() called" << endl; }
	~Final() { cout << "~Final() called" << endl; }
};
int main(int argc, char const *argv[])
{
	Final final;
	Base *pb;
	Class *pc;
	MI *pmi;
	D2 *pd2;
	// pb = new Class;//错误,Class是Base的基类,而pb是Base类。不能隐式地将基类指针转换为派生类指针。
	//报错:error: invalid conversion from 'Class*' to 'Base*'
	// pc = new Final;//报错:error: 'Class' is an ambiguous base of 'Final'
	//pmi = pb; //错误、pb是Base类,MI是Base的子类。不能隐式地将基类指针转换为派生类指针。
	pd2 = pmi;//派生类的指针可以转换为基类的指针。
	return 0;
}

测试:

Class() called
Base() called
D1() called
D2() called
MI() called
Class() called
Final() called
~Final() called
~Class() called
~MI() called
~D2() called
~D1() called
~Base() called
~Class() called

练习题18.30

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

class Class {};  
class Base :public Class {  
protected:  
    int ival;  
public:  
    Base() :ival(0),Class() {};  
    Base(const Base &b) = default;  
    Base(int a) :ival(a),Class() {}  
};  
class D1 :public virtual Base {  
public:  
    D1() :Base() {}  
    D1(const D1 &b) = default;  
    D1(int a) :Base(a) {}  
};  
class D2 :public virtual Base {  
public:  
    D2() :Base() {}  
    D2(const D2 &b) = default;  
    D2(int a) :Base(a) {}  
};  
class MI :public D1, public D2 {  
public:  
    MI() {}  
    MI(const MI &m) :Base(m), D1(m), D2(m) {}  
    MI(int i) :Base(i), D1(i), D2(i) {}  
};  
class Final :public MI, public Class {  
public:  
    Final() {}  
    Final(const Final &f) : Base(f), MI(f), Class() {}  
    Final(int i) : Base(i), Class() {}  
};  
发布了76 篇原创文章 · 获赞 44 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_24739717/article/details/104751868