C++ primer 第十三章習題

chapter13 拷貝、賦值與銷燬

練習

13.1.1 節練習

練習13.1

  • 拷貝構造函數是什麼?什麼時候使用它?

如果一個構造函數的第一個參數是自身類類型的引用,且任何額外參數都有默認值,則此構造函數是拷貝構造函數。

1)拷貝初始化(使用 = 初始化)

2)類作爲實參傳遞給非引用類型的形參

3)返回值類型爲非引用類型的函數返回一個對象時

4)用花括號列表初始化一個數組中的元素或一個聚合類中的成員

5)初始化標準庫容器或調用其insert或push成員,容器會對其元素進行拷貝初始化

練習13.2

  • 解釋爲什麼下面的聲明是非法的。
Sales_data::Sales_data( Sales_data rhs );

類的拷貝構造函數的第一個參數必須是引用類型。因爲其參數爲非引用類型時,爲了能夠調用拷貝構造函數,就必須拷貝它的實參,但拷貝其實參時,我們又需要調用拷貝構造函數,將導致無限循環。

練習13.3

  • 當我們拷貝一個StrBlob時,會發生什麼?拷貝一個StrBlobPtr呢?

對類類型的成員,將會調用其拷貝構造函數進行拷貝,內置類型成員則將會直接拷貝。

StrBlob:其數據成員僅有一個shared_ptr,因此將會拷貝這個shared_ptr,且其計數器自增1。

StrBlobPtr:其數據成員有一個weak_ptr以及一個size_t類型,curr將直接拷貝,weak_ptr將調用其類的拷貝構造函數進行拷貝,不增加其計數器。

練習13.4

  • 假定Point是一個類類型,它有一個public的拷貝構造函數,指出下面程序片段中哪些地方使用了拷貝構造函數。

  •   Point global;
      Point foo_bar( Point arg )  //用了一次拷貝構造函數(參數爲自身類類型的引用)
      {
      	Point local = arg, *heap = new Point( global );  //用了兩次拷貝構造函數 (=)
      	*heap = local;    //用了一次拷貝構造函數 (=)
      	Point pa[ 4 ] = { local, *heap };    //用了兩次拷貝構造函數,兩次用花括號列表初始化一個數組中的元素
      	return *heap;    //用了一次拷貝構造函數,從一個返回類型爲非引用類型的函數返回一個對象
      }
    

練習13.5

  • 給定下面的類框架,編寫一個拷貝構造函數,拷貝所有成員。你的構造函數應該動態分配一個新的string,並將對象拷貝到ps指向的位置,而不是ps本身的位置。

  •   class HasPtr{
      public:
      	HasPtr( const std::string &s = std::string() ) : 
      		ps( new std::string( s ) ), i(0) { }
      	HasPtr( const HasPtr &hp );
      private:
      	std::string *ps;
      	int i;
      };
    
HasPtr::HasPtr( const HasPtr & orig):
	ps( new std::string(*orig.ps) ), i(orig.i)  {  }

13.1.2 節練習

練習13.6

  • 拷貝賦值運算符是什麼?什麼時候使用它?合成拷貝賦值運算符完成什麼工作?什麼時候會生成合成拷貝賦值運算符?

拷貝賦值運算符是一個重載的賦值運算符,它是類的成員函數,其左側運算對象綁定到隱式的this參數。拷貝賦值運算符控制類對象如何賦值。拷貝賦值運算符接受一個與其所在類相同類型的參數,通常返回一個指向其左側運算對象的引用。

當用類對象對類對象進行賦值時,會使用拷貝運算符。

對某些類,合成拷貝賦值運算符用於禁止該類型對象的賦值,若非出於此目的,它會將左側運算對象的每個非static成員賦予左側運算對象的對應成員。

如果一個類未定義自己的拷貝賦值運算符,編譯器就會生成合成拷貝賦值運算符。

練習13.7

  • 當我們將一個StrBlob賦值給另一個StrBlob時,會發生什麼?賦值StrBlobPtr呢?

StrBlob:會通過shared_ptr類的拷貝賦值運算符將shared_ptr拷貝賦值,且其計數器自增。

StrBlobPtr:會通過weak_ptr類的拷貝賦值運算符將weak_ptr拷貝賦值。curr調用內置類型的賦值運算符。

練習13.8

  • 爲13.1.1節練習13.5中的HasPtr類編寫賦值運算符。類似拷貝構造函數,你的賦值運算符應該將對象拷貝到ps指向的位置。
class HasPtr{
public:
	HasPtr( const std::string &s = std::string() ) : 
		ps( new std::string( s ) ), i(0) { }
	HasPtr( const HasPtr &hp ) : ps( new std::string(*orig.ps) ), i(orig.i)  {  }
    HasPtr& operator=(const HasPtr &rhs_hp){
            if (this != &hp){    //判斷是否將自身賦值給自身
    	auto temp_ps = new std::string( *hp.ps );
    	delete ps;    //刪除原ps
    	ps = temp_ps;    //令ps指向新string
    	i = hp.i;
    }
    	return *this;
}
private:
	std::string *ps;
	int i;
};

13.1.3 節練習

練習13.9

  • 析構函數是什麼?合成析構函數完成什麼工作?什麼時候會生成合成析構函數?

析構函數釋放對象使用的資源,並銷燬對象的非static數據成員。析構函數是類的一個成員函數,名字由波浪號接類名構成,沒有參數,沒有返回值。

對於某些類,合成析構函數被用來阻止該類型的對象被銷燬,如果不是這種情況,合成析構函數的函數體就爲空。析構操作是在析構函數函數體執行完成後隱含執行的。

當一個類未定義自己的析構函數時,編譯器會爲它定義一個合成析構函數。

練習13.10

  • 當一個StrBlob對象被銷燬時會發生什麼?一個StrBlobPtr對象銷燬時呢?

StrBlob:合成析構函數的空函數體執行完成後,進入到隱含的析構階段,調用shared_ptr的析構函數來銷燬其指針,計數器減一,如果計數器爲0,銷燬其指向的對象。

StrBlobPtr:合成析構函數的空函數體執行完成後,進入到隱含的析構階段,調用weak_ptr的析構函數銷燬其指針。curr是內置類型,沒有析構函數,銷燬時什麼也不會發生。

練習13.11

  • 爲前面練習中的HasPtr類添加一個析構函數。
class HasPtr {
public:
	HasPtr(const std::string &s = std::string()) :
		ps(new std::string(s)), i(0) { }
	HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) {  }
	HasPtr& operator=(const HasPtr &rhs_hp) {
		if (this != &rhs_hp) {    //判斷是否將自身賦值給自身
			auto temp_ps = new std::string(*rhs_hp.ps);
			delete ps;    //刪除原ps
			ps = temp_ps;    //令ps指向新string
			i = rhs_hp.i;
		}
		return *this;
	}
    ~HasPtr(){    //析構函數
    delete ps;    //需要銷燬指針指向的內容
    }
private:
	std::string *ps;
	int i;
};

練習13.12

  • 在下面的代碼片段中會發生幾次析構函數的調用?

  •   bool fcn( const Sales_data *trans, Sales_data accum )
      {
      Sales_data item1( *trans ), item2( accum );
      return item1.isbn() != item2.isbn();
      }
    

一共三次。

局部變量item1, item2在函數fcn結束後銷燬。accum作爲複製的臨時變量,在函數fcn結束後也被銷燬。

trans作爲指針,沒有析構函數,同時也不會導致其指向的對象被銷燬。

練習13.13

  • 理解拷貝控制成員和構造函數的一個好方法是定義一個簡單的類,爲該類定義這些成員,每個成員都打印自己的名字:

  •   struct X {
      	X() { std::cout << "X()" << std::endl; }
      	X(const X&) { std::cout << "X(const X&)" << std::endl; }
      };
    
  • 給X添加拷貝賦值運算符和析構函數,並編寫一個程序以不同方式使用X的對象:

    將它們作爲非引用和引用參數傳遞;

    動態分配它們;

    將它們存放於容器中;

    諸如此類。觀察程序的輸出,直到你確認理解了什麼時候用使用拷貝控制成員,以及爲什麼會使用它們。當你觀察程序輸出時,記住編譯器可以略過對拷貝構造函數的調用。

#include <iostream>
#include <vector>

struct X {
	X() { std::cout << "X()" << std::endl; }
	X(const X&) { std::cout << "X(const X&)" << std::endl; }
	X& operator=(const X&) { std::cout << "X& operator=(const X&)" << std::endl; }
	~X() { std::cout << "~X" << std::endl; }
};

void f(const X& xr, X x) {
	std::vector<X> vx;
	vx.push_back(xr);
	vx.push_back(x);    //不知道爲什麼此處會多拷貝賦值一個X並析構掉。
}
int main() {
	X *x = new X();
	f(*x, *x);
	delete x;
	return 0;
}

13.1.4 節練習

練習13.14

  • 假定numbered是一個類,它有一個默認構造函數,能爲每個對象生成一個唯一的序號,保存在名爲mysn的數據成員中。假定numbered使用合成的拷貝控制成員,並給定如下函數:

  •   void f( numbered s ) { cout << s.mysn << endl; }
    
  • 則下面代碼輸出什麼內容?

  •   numbered a, b = a, c = b;
      f(a);f(b);f(c);
    

輸出三個相同的數字。使用合成拷貝控制成員,只會拷貝其序號,而不會生成新的唯一的序號。

練習13.15

  • 假定numbered定義了一個拷貝構造函數,能生成一個新的序號。這會改變上一題中調用的輸出結果嗎?如果會改變,爲什麼?新的輸出結果是什麼?

會改變。因爲b和c的初始化方式是拷貝初始化,通常使用的就是拷貝構造函數,因此上一題的輸出結果會改變。新的輸出結果將變爲三個不同的序號。對應的是abc三個實參拷貝生成的三個副本的mysn成員。

練習13.16

  • 如果f中的參數是const numered&, 將會怎樣? 這會改變輸出結果嗎?如果改變,爲什麼?

會改變。因爲將參數改爲引用後,不會再將實參拷貝而是直接傳入,因此輸出的是abc三個類的mysn成員。

練習13.17

  • 分別編寫前三題所描述的numbered和f,驗證你是否正確預測了輸出結果。
#include <iostream>
using namespace std;

class numbered1 {
public:
	numbered1() { mysn = unique++; }
	int mysn;
	static int unique;
};
class numbered2 {
public:
	numbered2() { mysn = unique++; }
	numbered2(const numbered2&) { mysn = unique++; }
	int mysn;
	static int unique;
};
int numbered1::unique = 10;
int numbered2::unique = 20;

void f(numbered1 s) { cout << s.mysn << endl; }
void f(numbered2 s) { cout << s.mysn << endl; }
void fc(const numbered2& s) { cout << s.mysn << endl; }
int main() {
	numbered1 a , b = a, c = b;
	f(a); f(b); f(c);
	numbered2 d, e = d, g = e;
	f(d); f(e); f(g);
	fc(d); fc(e); fc(g);
}

13.1.6 節練習

練習13.18

  • 定義一個Employee類,它包含僱員的姓名和唯一的僱員證號。爲這個類定義默認構造函數,以及接受一個表示僱員姓名的string的構造函數。每個構造函數應該通過遞增一個static數據成員來生成一個唯一的證號。
class Employee {
public:
	Employee() { id = unique++; };
	Employee(const std::string& str) : name(str) { id = unique++; };
private:
	std::string name;
	int id;
	static int unique;
};
int static unique = 0;

練習13.19

  • 你的Employee類需要定義它自己的拷貝控制成員嗎?如果需要,爲什麼?如果不需要,爲什麼?實現你認爲Employee需要的拷貝控制成員。

不需要。因爲在現實世界中,僱員不可能真正被拷貝,因此其唯一的僱員證號也就沒有必要因爲拷貝而自增。在使用時,使用其引用即可。 將控制拷貝成員刪除,避免沒有意義的拷貝。

class Employee {
public:
	Employee() { id = unique++; };
	Employee(const std::string& str) : name(str) { id = unique++; };
	Employee(const Employee&) = delete;
	Employee& operator = (const Employee&) = delete;
private:
	std::string name;
	int id;
	static int unique;
};

練習13.20

  • 解釋當我們拷貝、賦值或銷燬TextQuery和QueryResult類對象時會發生什麼?

練習13.21

  • 你認爲TextQuery和QueryResult類需要定義它們自己版本的拷貝控制成員嗎?如果需要,爲什麼?如果不需要,爲什麼?實現你認爲這兩個類需要的拷貝控制操作。

13.2 節練習

練習13.22

  • 假定我們希望HasPtr的行爲像一個值。即,對於對象所指向的string成員,每個對象都有一份自己的拷貝。我們將在下一節介紹拷貝控制成員的定義。但是,你已經學習了定義這些成員所需的所有知識。在繼續學習下一節之前爲HasPtr編寫拷貝構造函數和賦值運算符。
class HasPtr {
public:
	HasPtr(const std::string &s = std::string()) :
		ps(new std::string(s)), i(0) { }
	HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) {  }
	HasPtr& operator=(const HasPtr &rhs_hp) {
		if (this != &rhs_hp) {    //判斷是否將自身賦值給自身
			auto temp_ps = new std::string(*rhs_hp.ps);
			delete ps;    //刪除原ps
			ps = temp_ps;    //令ps指向新string
			i = rhs_hp.i;
		}
		return *this;
	}
    ~HasPtr(){    //析構函數
    delete ps;    //需要銷燬指針指向的內容
    }
private:
	std::string *ps;
	int i;
}; 

13.2.1 節練習

練習13.23

  • 比較上一節練習中你編寫的拷貝控制成員和這一節中的代碼。確定你理解了你的代碼和我們的代碼之間的差異(如果有的話)。

我的代碼中增加了一個是否是在將自身賦值給自己的判斷。這個判斷可以省略。

練習13.24

  • 如果本節中的HasPtr版本未定義析構函數,將會發生什麼?如果未定義拷貝構造函數,將會發生什麼?

如果沒有定義析構函數,會導致ps指向的動態內存無法釋放。

如果沒有定義拷貝控制函數,會導致拷貝時拷貝的是指針本身,而非連同動態內存一起拷貝。其結果就是當其中一個類成員被銷燬時,另一個類成員的ps將會變爲空懸指針。

練習13.25

  • 假定希望定義StrBlob的類值版本,而且希望繼續使用shared_ptr,這樣我們的StrBlobPtr類就仍能使用指向vector的weak_ptr了。你修改後的類將需要一個拷貝構造函數和一個拷貝賦值運算符,但不需要析構函數。解釋拷貝構造函數和拷貝賦值運算符必須要做什麼。解釋爲什麼不需要析構函數。

因爲需要的是類值版本,因此拷貝構造函數和拷貝賦值運算符需要令左側運算對象獲得一個新的底層vector,而不是令兩者指向一個相同的底層vector。

因爲StrBlob類內僅有一個shared_ptr成員,在類調用其合成析構函數就會再調用shared_ptr類的析構函數時,如果其計數器減至0了,就會將其指向的動態內存一同釋放。已經能夠保證資源分配、釋放的正確性。

練習13.26

  • 對上一題描述的StrBlob類,編寫你自己的版本。
#ifndef StrBlob_h
#define StrBlob_h

#include <string>
#include <vector>
#include <memory>
#include <exception>
class ConstStrBlobPtr;

class StrBlob {
	friend class ConstStrBlobPtr;
public:
	typedef std::vector<std::string>::size_type size_type;

	StrBlob();
	StrBlob(std::initializer_list<std::string> il);
	//拷貝構造函數
	StrBlob(const StrBlob& rhs) : data(std::make_shared<std::vector<std::string>>(*rhs.data)) { };
	//拷貝賦值運算符
	StrBlob& operator=(const StrBlob& rhs) { data = std::make_shared<std::vector<std::string>>(*rhs.data); return *this; };
	size_type size() const { return data->size(); }
	bool empty() const { return data->empty(); }
	// 添加刪減元素
	void push_back(const std::string &t) { data->push_back(t); }
	void pop_back() 
	{check(0, "pop_back on empty StrBlob");
		data->pop_back();}	
	// 元素訪問
	std::string& front();
	const std::string& front() const;
	std::string& back();
	const std::string& back() const;
	ConstStrBlobPtr begin() const;     //兩個函數改爲const成員函數
	ConstStrBlobPtr end() const;
private:
	std::shared_ptr < std::vector<std::string>> data;
	// 如果data[i]不合法,拋出一個異常
	void check(size_type i, const std::string &msg) const;
};

StrBlob::StrBlob() : data(std::make_shared<std::vector<std::string>>()) { }
StrBlob::StrBlob(std::initializer_list<std::string> il) : data(std::make_shared<std::vector<std::string>>(il)) { }

void StrBlob::check(size_type i, const std::string &msg) const {
	if (i >= data->size())
		throw std::out_of_range(msg);
}

std::string& StrBlob::front(){
	// 如果vector爲空, check會拋出一個異常
	check(0, "front on empty StrBolb");
	return data->front();
}

std::string& StrBlob::back() {
	check(0, "back on empty StrBlob");
	return data->back();
}

const std::string& StrBlob::front() const {
	// 如果vector爲空, check會拋出一個異常
	check(0, "front on empty StrBolb");
	return data->front();
}

const std::string& StrBlob::back() const {
	check(0, "back on empty StrBlob");
	return data->back();
}


class ConstStrBlobPtr {
public:
	ConstStrBlobPtr() : curr(0) {}
	ConstStrBlobPtr(const StrBlob &a, std::size_t sz = 0) : wptr(a.data), curr(sz) {}    // 接受參數改爲const StrBlob&
	const std::string& deref() const;    //根據邏輯,此處的返回值應爲const
	ConstStrBlobPtr& incr(); //前綴遞增
	bool operator!=(const ConstStrBlobPtr &a) { return a.curr != curr; }
private:
	std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;
	std::weak_ptr<std::vector<std::string>> wptr;
	std::size_t curr;
};

std::shared_ptr<std::vector<std::string>> ConstStrBlobPtr::check(std::size_t i, const std::string &msg) const {
	auto ret = wptr.lock();    // vector還存在嗎
	if (!ret)
		throw std::runtime_error("unbound StrBlobPtr");
	if (i >= ret->size())
		throw std::out_of_range(msg);
	return ret;    // 一切正常,返回指向vector的shared_ptr
}

const std::string& ConstStrBlobPtr::deref() const {
	auto p = check(curr, "dereference past end");
	return (*p)[curr];
}

ConstStrBlobPtr& ConstStrBlobPtr::incr() {    // 前綴遞增,返回遞增後對象的引用
	check(curr, "increment past end of StrBlobPtr");
	++curr;
	return *this;
}

ConstStrBlobPtr StrBlob::begin() const { 
	return ConstStrBlobPtr(*this);
}

ConstStrBlobPtr StrBlob::end() const {
	return ConstStrBlobPtr(*this, data->size());
}

#endif // StrBlob_h

13.2.2 節練習

練習13.27

  • 定義你自己的使用引用計數版本的HasPtr。
class HasPtr {
public:
	HasPtr(const std::string &s = std::string()) :
		ps(new std::string(s)), i(0) { }
	//    拷貝構造函數拷貝所有三個數據成員,並遞增計數器
	HasPtr(const HasPtr &hp) : ps(hp.ps), i(hp.i), use(hp.use) { ++*use; }
	HasPtr& operator=(const HasPtr &rhs_hp) {
		++*rhs_hp.use;    //先自增右側對象的計數器,再自減左側對象計數器。來處理自賦值的情況。
		if (--*use == 0) {
			delete ps;    //需要銷燬指針指向的內容
			delete use;
		}
		ps = rhs_hp.ps;
		i = rhs_hp.i;
		use = rhs_hp.use;
		return *this;
	}
	~HasPtr() {    //析構函數
		if (--*use == 0) {
			delete ps;    //需要銷燬指針指向的內容
			delete use;
		}
	}
private:
	std::string *ps;
	int i;
	std::size_t *use;    //用來記錄有多少個對象共享*ps的成員
};

練習13.28

  • 給定下面的類,爲其實現一個默認構造函數和必要的拷貝控制成員。
(a)
class TreeNode {
private:
	std::string value;
	int *count;    // 此處應是指針
	TreeNode *left;
	TreeNode *right;
};
(b)
class BinStrTree {
private:
	TreeNode *root;
};
#include <string>

class TreeNode {
public:
	TreeNode() : value(std::string()), count(new int(1)), left(nullptr), right(nullptr) {}
	TreeNode(const TreeNode &rhs) : value(rhs.value), count(rhs.count), left(rhs.left), right(rhs.right) { ++*count; }
	TreeNode& operator=(const TreeNode &rhs) {
		++*rhs.count;
		if (--*count == 0) {
			delete count;
			delete left;
			delete right;
		}
		value = rhs.value;
		count = rhs.count;
		left = rhs.left;
		right = rhs.right;
		return *this;
	}
	~TreeNode() {
		if (--*count == 0) {
			delete count;
			delete left;
			delete right;
		}
	}
private:
	std::string value;
	int *count;
	TreeNode *left;
	TreeNode *right;
};

class BinStrTree {
public:
	BinStrTree() : root(new TreeNode()) { }
	BinStrTree(const BinStrTree& rhs) : root(new TreeNode(*rhs.root)) { }
	BinStrTree& operator=(const BinStrTree& rhs) {
		TreeNode *new_root = new TreeNode(*rhs.root);
		delete root;
		root = new_root;
		return *this;
	}
	~BinStrTree() {
		delete root;
	}
private:
	TreeNode *root;
};

13.3 節練習

練習13.29

  • 解釋swap( HasPtr&, HasPtr& )中對swap的調用不會導致遞歸循環。
inline void swap( Hasptr &lhs, HasPtr &rhs)
{   using std::swap;
    swap( lhs.ps, rhs.ps );
    swap( lhs.i, rhs.i );}

從代碼可以知道,下面兩個swap函數的參數分別是字符串指針和常數,並不是HasPtr&類型,自然不會導致遞歸循環。

練習13.30

  • 爲你的類值版本的HasPtr編寫swap函數,並測試它。爲你的swap函數添加一個打印語句,指出函數什麼時候執行。
class HasPtr {
    friend void swap(HasPtr&, HasPtr&);
public:
	HasPtr(const std::string &s = std::string()) :
		ps(new std::string(s)), i(0) { }
	HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) {  }
	HasPtr& operator=(const HasPtr &rhs_hp) {
		if (this != &rhs_hp) {    //判斷是否將自身賦值給自身
			auto temp_ps = new std::string(*rhs_hp.ps);
			delete ps;    //刪除原ps
			ps = temp_ps;    //令ps指向新string
			i = rhs_hp.i;
		}
		return *this;
	}
    ~HasPtr(){    //析構函數
    delete ps;    //需要銷燬指針指向的內容
    }
    
private:
	std::string *ps;
	int i;
}; 
inline void swap(HasPtr &lhs, HasPtr &rhs){
    using std::swap;
    swap(lhs.ps, rhs.ps);
    swap(lhs.i, rhs.i);
    std::cout << "swap" << std::endl;
}

練習13.31

  • 爲你的HasPtr類定義一個<運算符,並定義一個HasPtr的vector。爲這個vector添加一些元素,並對它執行sort。注意何時調用swap。
class HasPtr {
    friend void swap(HasPtr&, HasPtr&);
public:
	HasPtr(const std::string &s = std::string()) :
		ps(new std::string(s)), i(0) { }
	HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) {  }
	HasPtr& operator=(const HasPtr &rhs_hp) {
		if (this != &rhs_hp) {    //判斷是否將自身賦值給自身
			auto temp_ps = new std::string(*rhs_hp.ps);
			delete ps;    //刪除原ps
			ps = temp_ps;    //令ps指向新string
			i = rhs_hp.i;
		}
		return *this;
	}    // <運算符
    bool operator<(const HasPtr &lhs, const HasPtr &rhs){
        return *lhs.ps < *rhs.ps;
    }
    ~HasPtr(){    //析構函數
    delete ps;    //需要銷燬指針指向的內容
    }
    
private:
	std::string *ps;
	int i;
}; 
inline void swap(HasPtr &lhs, HasPtr &rhs){
    using std::swap;
    swap(lhs.ps, rhs.ps);
    swap(lhs.i, rhs.i);
    std::cout << "swap" << std::endl;
}

練習13.32

  • 類指針的HasPtr版本會從swap函數受益嗎?如果會,得到了什麼益處?如果不是,爲什麼?

自定義的swap的目的是爲了減少swap過程中不必要的臨時副本的創建,而類指針的HasPtr版本,僅有一個int類型可能需要創建臨時副本,其餘均是直接交換指針地址。因此基本不會得到益處。

13.4 節練習

練習13.33

  • 爲什麼Message的成員save和remove的參數是一個Folder&?爲什麼我們不將參數定義爲Folder或者是const Folder&?

將參數定義爲Folder,那麼save和remove將對副本進行操作,等於做無用功。

將參數定義爲const Folder&,那麼引用將無法被改變,將無法完成函數的功能。

練習13.34

  • 編寫本節所描述的Message。
#ifndef Message_h
#define Message_h

#include <set>
class Folder;

class Message{
	friend class Folder;
	friend void swap(Message&, Message&);
public:
	// folder 被隱式初始化爲空集合
	explicit Message(const std::string &str = "") : contents(str) {}  
	// 拷貝控制成員,用來管理指向本Message的指針
	Message(const Message&);
	Message& operator=(const Message&);
	~Message();
	// 從給定Folder集合中添加/刪除本Message
	void save(Folder&);
	void remove(Folder&);

private:
	std::string contents;
	std::set<Folder*> folders;    // 包含本Message的Folder
	// 拷貝控制成員所使用的的工具函數
	void add_to_Folders(const Message&);
	void remove_from_Folders();
	void addFldr(Folder *f) { folders.insert(f); }
	void remFldr(Folder *f) { folders.erase(f); }
};

class Folder { 
	friend class Message;
	friend void swap(Folder&, Folder&);
public:
	Folder() = default;
	// 拷貝控制成員
	Folder(const Folder&);
	Folder& operator=(const Folder&);
	~Folder();
private:
	std::set<Message*> msgs;
	void add_to_Message(const Folder&);
	void remove_from_Message();
	void addMsg(Message *m) { msgs.insert(m); };
	void remMsg(Message *m) { msgs.erase(m); };
};

void Message::save(Folder &f) {
	folders.insert(&f);    // 將給定Folder添加到我們的Folder列表中
	f.addMsg(this);    // 將本Message添加到f的Message集合中
}

void Message::remove(Folder &f) {
	folders.erase(&f);    // 將給定Folder從我們的Folder列表中刪除
	f.remMsg(this);    // 將本Message從f的Message集合中刪除
}
// 將本Message添加到指向m的Floder中
void Message::add_to_Folders(const Message &m) {
	for (auto f : m.folders)    // 對每個包含m的Folder
		f->addMsg(this);    // 向改Folder添加一個指向本Message的指針
}

Message::Message(const Message &m) :
	contents(m.contents), folders(m.folders) {
	add_to_Folders(m);    // 將本消息添加到指向M的Folder中
}

void Message::remove_from_Folders() {
	for (auto f : folders)    // 對folders中每個指針
		f->remMsg(this);    // 從該Folder中刪除本Message
}

Message::~Message() {
	remove_from_Folders();
}

Message& Message::operator=(const Message &rhs) {
	// 通過先刪除自身指針再插入它們來處理自賦值情況
	remove_from_Folders();    // 更新已有Folder
	contents = rhs.contents;    // 從rhs拷貝消息和Folder指針
	folders = rhs.folders;
	add_to_Folders(rhs);    // 將本Message添加到那些Folder中
	return *this;
}

void swap(Message &lhs, Message &rhs) {
	using std::swap;
	// 將每個消息的指針從它(原來)所在的Folder中刪除
	lhs.remove_from_Folders();
	rhs.remove_from_Folders();
	// 交換contents和Folder指針set
	swap(lhs.folders, rhs.folders);    // 使用swap(set&, set&)
	swap(lhs.contents, rhs.contents);    // 使用swap(string&, string&)
	// 將每個Message的指針添加到它的(新)Folder中
	lhs.add_to_Folders(lhs);    // 使用工具函數避免代碼重複
	rhs.add_to_Folders(rhs);
}
// 將自身添加到參數Folder中的Message的Folder集合
void Folder::add_to_Message(const Folder &f) {
	for (auto m : f.msgs)
		m->addFldr(this);
}
// 將自身從自身的Message集合中刪除
void Folder::remove_from_Message() {
	for (auto m : msgs)
		m->remFldr(this);
}

Folder::Folder(const Folder &f) : msgs(f.msgs) {
	add_to_Message(f);
}

Folder::~Folder() {
	remove_from_Message();
}

Folder& Folder::operator=(const Folder&f) {
	remove_from_Message();    // 避免自賦值
	msgs = f.msgs;
	add_to_Message(f);
	return *this;
}

void swap(Folder &lhs, Folder &rhs) {
	lhs.remove_from_Message();
	rhs.remove_from_Message();
	swap(lhs.msgs, rhs.msgs);
	lhs.add_to_Message(lhs);
	rhs.add_to_Message(rhs);
}

#endif // !Message_h

練習13.35

  • 如果Message使用合成的拷貝控制成員,將會發生什麼?

如果Message使用合成的拷貝控制成員,那麼Folder將不會知道Message被拷貝或銷燬了,會導致Folder中記錄的Message數量與Folder中記錄的不符合。

練習13.36

  • 設計並實現對應的Folder類。此類應該保存一個指向Folder中包含的Message的set。

練習13.37

  • 爲Message類添加成員,實現向 folders添加或刪除一個給定的Folder。這兩個成員類似Folder類中的addMsg和remMsg操作。*

以上兩題見13.34的代碼。

練習13.38

  • 我們並未使用拷貝和交換的方式來設計Message的賦值運算符。你認爲其原因是什麼?

沒有動態內存的使用,因此使用拷貝和交換的方式並不能得到多少優化。另外我們Message的swap函數還會修改保存該Message的Folder中的成員,無法直接使用,需要修改。不值得爲其再修改一個交換的函數,效率過低。

13.5 節練習

練習13.39

  • 編寫你自己版本的StrVec,包括你自己版本的reserve、capacity 和 resize。
#ifndef StrVec_h
#define StrVec_h

#include <allocators>
#include <string>
#include <initializer_list>

// 類vector類內存分配策略的簡化實現
class StrVec {
public:
	// allocator成員進行默認初始化
	StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) { }
	StrVec(const StrVec&);               // 拷貝構造函數
	StrVec(std::initializer_list<std::string>);
	StrVec& operator=(const StrVec&);    // 拷貝賦值運算符
	~StrVec();                           // 析構函數
	StrVec(StrVec &&rhs) noexcept;    // 移動構造函數
	StrVec& operator=(StrVec&&rhs) noexcept;    // 移動賦值運算符
	void push_back(const std::string&);  // 拷貝元素

	size_t size() const { return first_free - elements; }
	size_t capacity() const { return cap - elements; }
	std::string *begin() const { return elements; }
	std::string *end() const { return first_free; }

	void reserve(size_t);
	void resize(size_t);
	void resize(size_t, const std::string&);

private:
	static std::allocator<std::string> alloc;    // 分配元素
	std::string *elements;      // 指向數組首元素的指針
	std::string *first_free;    // 指向數組第一個空閒元素的指針
	std::string *cap;           // 指向數組尾後位置的指針

	// 被添加元素的函數所使用
	void chk_n_alloc() { if (size() == capacity()) reallocate(); }
	// 工具函數,被拷貝構造函數、拷貝賦值運算符和析構函數所使用
	std::pair<std::string*, std::string*> alloc_n_copy(const std::string*, const std::string*);
	void free();                // 銷燬元素並釋放內存
	void reallocate();          // 獲得更多內存並拷貝已有元素
	void alloc_n_move(size_t);    // 將元素移至擴增容量後的新內存地址的工具函數
	void range_initialize(const std::string*, const std::string*);
};

void StrVec::push_back(const std::string &s) {
	chk_n_alloc();     // 確保有空間容納新元素
	// 在first_free指向的元素中構造s的副本
	alloc.construct(first_free++, s);
}

std::pair<std::string*, std::string*>
StrVec::alloc_n_copy(const std::string *b, const std::string *e) {
	// 分配空間保存給定範圍內的元素
	auto data = alloc.allocate(b - e);
	// 初始化並返回一個pair,該pair由data和uninitialized_copy的返回值構成
	return { data, std::uninitialized_copy(b, e, data) };
}

void StrVec::free() {
	// 不能傳遞給deallcate一個空指針,如果elements爲0,函數什麼也不做
	if (elements) {
		// 逆序銷燬元素
		for (auto p = first_free; p != elements;/* 空 */)
			alloc.destroy(--p);
		alloc.deallocate(elements, cap - elements);
	}
}

void StrVec::range_initialize(const std::string *first, const std::string *last) {
	auto newdata = alloc_n_copy(first, last);
	elements = newdata.first;
	first_free = cap = newdata.second;
}

StrVec::StrVec(const StrVec &s) {
	// 調用alloc_n_copy分配空間以容納與 s 中一樣多的元素
	range_initialize(s.begin(), s.end());
}

StrVec::StrVec(std::initializer_list<std::string> il) {
	range_initialize(il.begin(), il.end());
}

StrVec::~StrVec() { free(); }

StrVec& StrVec::operator=(const StrVec &rhs) {
	// 調用alloc_n_copy分配內存,大小與rhs中元素佔用的空間一樣大
	auto data = alloc_n_copy(rhs.begin(), rhs.end());
	free();
	elements = data.first;
	first_free = cap = data.second;
	return *this;
}

StrVec::StrVec(StrVec &&rhs) noexcept : elements(rhs.elements), first_free(rhs.first_free), cap(rhs.cap) {
	rhs.elements = rhs.first_free = rhs.cap = nullptr;    // 使右值引用可以安全的被析構
}

StrVec& StrVec::operator=(StrVec &&rhs) noexcept {
	if (this != &rhs)    // 直接檢測自賦值
	{
        free();
		elements = rhs.elements;
		first_free = rhs.first_free;
		cap = rhs.cap;
		rhs.elements = rhs.first_free = rhs.cap = nullptr;
	}
	return *this;
}

void StrVec::alloc_n_move(size_t newcapacity) {
	// 分配新內存
	auto newdata = alloc.allocate(newcapacity);
	// 將數據從舊內存移動到新內存
	auto dest = newdata;          // 指向新數組中的下一個空閒 
	auto elem = elements;         // 指向舊數組中的下一個元素
	for (size_t i = 0; i != size(); ++i)
		alloc.construct(dest++, std::move(*elem++));
	free();     // 一旦我們移動完元素就釋放舊內存空間
	// 更新我們的數據結構,執行新元素
	elements = newdata;
	first_free = dest;
	cap = elements + newcapacity;
}

void StrVec::reallocate() {
	// 我們將分配當前大小兩倍的內存空間
	auto newcapacity = size() ? 2 * size() : 1;
	alloc_n_move(newcapacity);
}

void StrVec::reserve(size_t newcapacity) {
	if (newcapacity > capacity())     // 如果要求分配的空間小於等於已分配空間,不做改變
		alloc_n_move(newcapacity);
}

void StrVec::resize(size_t count) {
	resize(count, std::string());
}

void StrVec::resize(size_t count, const std::string &str) {
	if (count < size()) {
		while (first_free != elements + count)
			alloc.destroy(--first_free);
	}
	else if (count > size()) {
		alloc_n_move(count * 2);
		while (first_free != elements + count)
			alloc.construct(first_free++, str);
	}
}

#endif // !StrVec_h

練習13.40

  • 爲你的StrVec類添加一個構造函數,它接受一個initializer_list的參數。

見練習13.39

練習13.41

  • 在push_back中,我們爲什麼在construct調用使用前置遞增運算?如果使用後置遞增運算的話,會發生什麼?

我覺得用的是後置版本啊。。。爲了在first_free位置創建新元素,因爲first_free的位置就是一般意義上的尾後位置,因此先再first_free位置創建新元素再向後移動纔是合理的。用前置版本,將會先移動first_free,再在移動後的位置上創建新元素,就不合理了。

練習13.42

  • 在你的TextRequery和QueryResult類中用你的StrVec類代替vector,以此來測試你的StrVec類。

沒做,略。

練習13.43

  • 重寫free成員,用for_each和lambda來代替for循環destroy元素。你更傾向於哪種實現?爲什麼?
for_each(elements, first_free, [](std::string &s){ alloc.destroy(&s); });

for_each的實現,更簡潔,可讀性更強,也不需要人爲控制指針,減少了出錯的可能性。

練習13.44

  • 編寫標準庫string類的簡化版本,命名爲String。你的類至少有一個默認構造函數和一個接受C風格字符指針參數的構造函數。使用allocator爲你的String類分配所需內存。
#ifndef String_h
#define String_h

#include <memory>
#include <algorithm>
#include <iostream>

class String{
public:
	String(): String("") {}    // 委託構造函數
	String(const char*);
	String(const String&);
	String& operator=(const String&);
	String(String&&)noexcept;    // 移動構造函數
	String& operator=(String&&)noexcept;    // 移動賦值運算符
	~String();
private:
	char *elements;
	char *end;
	static std::allocator<char> alloc;

	std::pair<char*, char*> alloc_n_copy(const char*, const char*);
	void free();
	void print();
};

std::pair<char*, char*> String::alloc_n_copy(const char* b, const char* e) {
	auto data = alloc.allocate(e - b);
	return { data, std::uninitialized_copy(b,e,data) };
}

String::String(const char* c) {
	auto e = c;
	while (*e != '\0') e++;    // 找到字符串的長度
	e++;
	auto data = alloc_n_copy(c, e);
	elements = data.first;
	end = data.second;
	print();
}

String::String(const String &rhs) {
	auto data = alloc_n_copy(rhs.elements, rhs.end);
	elements = data.first;
	end = data.second;
	print();
}

void String::free() {
	if (elements) {
		std::for_each(elements, end, [](char &c) {alloc.destroy(&c); });
		alloc.deallocate(elements, end - elements);
	}
}

String& String::operator=(const String &rhs) {
	auto data = alloc_n_copy(rhs.elements, rhs.end);
	free();
	elements = data.first;
	end = data.second;
	return *this;
}

String::~String() { free(); }

String::String(String &&rhs) noexcept : elements(rhs.elements), end(rhs.end) {
	rhs.elements = rhs.end = nullptr;    // 將右側對象置爲可析構的狀態
}

String& String::operator=(String &&rhs) noexcept {
	if (this != &rhs) {    // 檢測自賦值
		free();
		elements = rhs.elements;
		end = rhs.end;
		rhs.elements = rhs.end = nullptr;
	}
	return *this;
}
void String::print() {
	std::cout << "This is a String." << std::endl;
}
#endif // !StrVec_h

13.6.1 節練習

練習13.45

  • 解釋右值引用和左值引用的區別。

右值引用用&&來獲取,它是必須綁定到右值的引用,只能綁定到一個將要銷燬的對象。

左值引用(即常規引用)不能綁定到要求轉換的表達式、字面常量或返回右值的表達式,而右值引用則相反。

左值引用用以保存持久的狀況。而右值引用要麼是保存字面常量,要麼是表達式求值過程中創建的臨時變量。

練習13.46

  • 什麼類型的引用可以綁定到下面的初始化器上?

  •   int f();
      vector<int> vi(100);
      int? r1 = f();
      int? r2 = vi[0];
      int? r3 = r1;
      int? r4 = vi[0] * f();
    
int&& r1 = f();           // f()返回右值,因此需以右值引用來綁定
int& r2 = vi[0];          // 變量表達式,以左值引用綁定
int& r3 = r1;             // 綁定了右值引用的變量,以左值引用綁定
int&& r4 = vi[0] * f();    // 返回右值的表達式,以右值引用綁定

練習13.47

  • 對你在練習13.44中定義的String類,爲它的拷貝構造函數和拷貝賦值運算符添加一條語句,在每次函數執行時打印一條信息。

已添加在練習13.44中。

練習13.48

  • 定義一個vector並在其上多次調用push_back。運行你的程序,並觀察String被拷貝了多少次。

略。

13.6.2 節練習

練習13.49

  • 爲你的StrVec、String和Message類添加一個移動構造函數和一個移動賦值運算符。

StrVec見練習13.39。String見練習13.44。Message略。

練習13.50

  • 在你的String類的移動的操作中添加打印語句,並重新運行13.6.1節的練習13.48中的程序,它使用了一個vector,觀察什麼時候會避免拷貝。

返回右值的時候。

練習13.51

  • 雖然unique_ptr不能拷貝,但我們在12.1.5節中編寫了一個clone函數,它以值方式返回一個unique_ptr。解釋爲什麼函數是合法的,以及爲什麼它能工作。

因爲以值方式返回的unique_ptr是右值,因此在返回它時,它會調用unique_ptr內的移動構造函數或移動賦值運算符來接管此函數中unique_ptr變量的所屬權,因此是合法的。

練習13.52

  • 詳細解釋第478頁中HasPtr對象的賦值發生了什麼?特別是,一步一步描述hp、hp2以及HasPtr的賦值運算符中的參數rhs的值發生了什麼變化?

對於hp = hp2:

hp2是一個左值,因此在參數傳遞時,構造形參rhs調用的是拷貝構造函數,rhs將獲得hp2的一個副本,rhs和hp2是獨立的,而且string的內容相同。賦值結束後,rhs被銷燬。

對於hp = std::move( hp2 );

構造rhs的時候調用的是移動構造函數,rhs接管hp2的所有權,然後swap交換this和rhs的指針,rhs指向this中數據成員原來指向的string,而this數據成員指向hp2指向的string。

練習13.53

  • 從底層效率的角度看,HasPtr的賦值運算符並不理想,解釋爲什麼。爲HasPtr實現一個拷貝賦值運算符和一個移動賦值運算符,並比較你的新的移動賦值運算符中執行的操作和拷貝並交換版本中執行的操作。

因爲當賦值運算符獲得一個右值引用時,它依然會先拷貝一個對象,再進行交換操作,而不是直接與右值引用進行交換。

練習13.54

  • 如果我們爲HasPtr定義了移動賦值運算,但未改變拷貝並交換運算符,會發生什麼?編寫代碼驗證你的答案。

編譯不通過。

error: ambiguous overload for 'operator=' (operand types are 'HasPtr' and 'std::remove_reference<HasPtr&>::type { aka HasPtr }')
hp1 = std::move(*pH);

13.6.3 節練習

練習13.55

  • 爲你的StrBlob添加一個右值引用版本的push_back。
void push_back(string &&t) { data->push_back(std::move(t)); }

練習13.56

  • 如果sorted定義如下,會發生什麼:

  •   Foo Foo::sorted() const & {
          Foo ret(*this);
          return ret.sorted();
      }
    

    因爲通過拷貝構造函數生成的ret是一個左值,因此會繼續調用當前版本的sorted函數,直至內存泄露。

練習13.57

  • 如果sorted定義如下,會發生什麼:

  •   Foo Foo::sorted() const & { return Foo(*this).sorted(); }
    

Foo(*this)返回的是臨時變量,是一個右值,因此會正確調用右值引用版本的sorted。

練習13.58

  • 編寫新版本的Foo類,其sorted函數中有打印語句,來驗證你對前面兩題的答案是否正確。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章