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() {}  
};  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章