C++自修入門實境秀、C++ Primer 5版研讀秀 105/ ~第13章 拷貝控制13.4. A Copy-Control Example-...



13.2.2. Defining Classes That Act Like Pointers

定義有著類指標行為的類別

第105集 6:45:10

要讓HasPtr類別行為表現得像個指標,我們就必須將其拷貝建構器及拷貝指定運算子定義成為去拷貝其指標成員的本身,而不是去拷貝它指標成員指向的string物件。這樣的HasPtr類別仍然需要它自己的解構器來解構由其帶了一個string作為引數而以new配置string的建構器所建置出來的資源。(§13.1.4,頁505)然而解構器在這類指標的類別行為下並不能自行決定釋放ps所指向的string物件的時機。它必須要在最後一個指向那個string的HasPtr物件消失之後,才能將其解構,以免其他的HasPtr物件內的ps還在參考那個建構器new出來的string。

想讓一個類別實現類指標的功能,最簡易的方式就是將類別管控其i資料成員資源的工作交給(委任、委派)shared_ptr來代做。對sahred_ptr進行拷貝或指定,其實就是拷貝或指定shared_ptr指向的那個指標。(應即前文所謂shared_ptr底層的指標。因為shared_ptr是智慧指標,這種指標指向的也是個指標,是普通指標)

Copying (or assigning) a shared_ptr copies (assigns) the pointer to which the shared_ptr points.

可見shared_ptr的原理是shared_ptr指向一個普通指標,然後這個普通指標再指向那個物件(未必是動態配置物件)。

shared_ptr它自己會追蹤到底有多少個shared_ptr在共享它所指向的這個物件。一旦不再有任何shared_ptr再指向這個物件時,它就會自行解構(即調用該物件型別的解構器來解構)該物件並釋放這物件所佔用的資源。

頁514

有時我們也會自己來直接管控類別資料成員的資源,而不借重shared_ptr來代我們託管;這時利用reference count(參考計數 §12.1.1,頁452)的技術或方式來管控new動態配置出來的資源是個好辦法。為了要說明參考計數(reference counting)是怎麼作用的,我們將重新定義HasPtr類別,讓它表現得像個指標,但我們卻不會用shared_ptr來管理HasPtr的資源,而是用我們自己自訂的參考計數來管控。

參考計數(reference counts)

參考計數會有如下的行為:

6:58:24

 除了對資料成員作初始化外,類別的每個建構器(除了拷貝建構器)都會創建一個計數器(counter)。這個計數器就是用來專門記錄有多少已被建置出來的類別物件,與現在要建置的這個在共享資源。只要我們建置一個物件,當然就只有一個物件在使用配置出來的資源,因此我們就將這個計數器的值,初始化為1。

 拷貝建構器當然就不需要建置這個計數器,它的工作則是負責將建置物件時配置好的計數器連同類別的資料成員一起作拷貝。它會遞增這個計數器來表示已經有另一個物件在共享這個底層的資源。

 解構器則是負責遞減這個計數器,來表示資源共享者又少了一員。如果這個計數器的值歸零,解構器才會也才能去將原在共享的資源給清除。

 拷貝指定運算子則會將其右運算元的計數器遞增,而將左邊的遞減。一旦遞減至零,拷貝指定運算子就必須將左運算元的資源給清除乾淨。7:21:07

這一部分比較傷腦筋的是要將這個計數器定義在哪裡才好。這個計數器勢必不能是該類別物件的成員本身。要知道為什麼,只要想想執行像下列的程式碼會發生什麼情形:

HasPtr p1("Hiya!");

HasPtr p2(p1); //此時p1和p2同時指向同一個string

HasPtr p3(p1); //p1、p2、p3全都指向同一個string

7:23:15

如果參考計數是存放在每個類別物件中,那麼在p3被建置時我們又要怎麼去更新這個計數,才不會有所遺漏?如我們可以遞增p1內的計數器,並把它複製給p3,但此時,我們又要怎麼去更新p2內的計數呢?

解決這個困境的一個辦法是將這個計數器放到動態記憶體(dynamic memory)中。當我們建置一個類別物件時,我們同時也建置出一個新的計數器。而當我們對此類別物件進行拷貝或指定時,我們就拷貝指向這個計數器的指標。這樣一來,拷貝後的指標就會與拷貝來源的指標指向相同的一個計數器了。

Defining a Reference-Counted Class

定義有著參考計數的類別

7:25:56

有了參考計數,我們就可以將HasPtr像下面這樣定義成有著類指標的行為:

頁515

class HasPtr

{

public:

//建構器配置出一個新的string和計數器,並將這個計數器的值初始化為1

HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0), use(new std::size_t(1)) {}

//拷貝建構器則是拷貝此類別中全部的3個資料成員(包括那個計數器),並會遞增計數器

HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use) { ++*use; }

HasPtr &operator=(const HasPtr &);

~HasPtr();



private:

std::string *ps;

int i;

std::size_t *use; //這個資料成員是用來追蹤到底有多少物件共享ps解參考後的這個string

};

7:27:20

這裡我們加入了一個新的資料成員叫做use作為計數器來追蹤有多少類別物件在共用同一個string的物件。而帶了一個string作引數的建構器則會創建出use這個計數器並將其值初始化為1,以表示目前對於這個新物件的ps成員所指向的string唯有一個使用者--就是這個新物件自己。

Pointerlike Copy Members “Fiddle” the Reference Count

類指標行為的類別其拷貝控制成員會去觸發、觸動、撥動參考計數

類指標的拷貝成員讓參考計數有點「費勁」(中文版)

當對HasPtr物件進行拷貝或指定時,我們會希望拷貝或指定的雙方能指向同一個string物件。也就是說,拷貝時,我們要拷貝指標成員ps本身,而不是去拷貝它所指向的string。且拷貝的同時,我們也要遞增參考到那個string的參考計數。

7:33:55

拷貝建構器(在類別內已定義完畢了)會從來源物件將其內3個資料成員全部拷貝過來。這個建構器也會遞增計數器use來表示現在又多了一個對p.ps指向的string的使用者——ps了(其實就是多了ps,與p.ps共用這個string。英文版寫成「This constructor also increments the use member, indicating that there is another user for the string to which ps and p.ps point.」是有點誤導,或根本是錯誤的!)。

7:40:25

而解構器則不允許恣意對ps進行delete的運算,因為也許還有別的使用者需要用到ps所指向的string物件。因此,解構器必須先遞減use這個參考計數器,來表示共用這個string的類別物件又少了一個。只有到了這個參考計數器的值歸了零,解構器才能去對ps和use作delete運算來釋放string和size_t所佔用的記憶體資源:

7:42:13

HasPtr::~HasPtr()

{

if (--*us == 0)

{ //一旦參考計數歸零

delete ps; //對ps下delete運算子,以釋放ps所指向的string

delete use; //也釋放use所指向的size_t所佔用的記憶體資源

}

}

而拷貝指定運算子通常都會做到拷貝建構器與解構器所做的事。也就是說指定運算子必須要能遞增其右運算元的參考計數(一如拷貝建構器所做的那樣)並且能夠遞減其左運算元的,且能在左運算元的參考計數歸零時清除它原有的資源(一如解構器所做的那樣)。

一樣地,指定運算子也必須能夠正確無誤地處理自拷貝或自指定才行。故我們要先將右運算元(即rhs)的參考計數遞增1,然後再遞減左運算元的參考計數。

頁516

這樣一來,若左、右運算元是同一個東西,那麼我們就會在決定是否要對ps和use執行delete運算前,就已經先遞增右運算元的參考計數器了——否則若先行清除這個use計數器,再擬遞增其計數,不就必然會發生錯誤了;也就是還需要用到它,怎麼可以先行清除它呢?:

HasPtr &HasPtr::operator=(const HasPtr &rhs)

{

++*rhs.use; //遞增右運算元的參考計數

if (--*use == 0)

{ //然後才遞減指定目的地的物件(即左運算元)的參考計數

delete ps; //一旦沒有任何使用者了

delete use; //就解構左運算元中的由new動態配置的資料成員

}

ps = rhs.ps; //拷貝/指定rhs引數的成員給左運算元

i = rhs.i;

use = rhs.use;

return *this; //拷貝/指定完成,就回傳這個左運算元該有的物件

}

練習13.27

請自行定義具備參考計數功能的HasPtr類別。參考計數(reference count)

忘了錄的部分就看臉書直播第541集 https://www.facebook.com/oscarsun72/videos/2659467894164328/

詳:https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise13_27/prog1

練習13.28

第105集開始

38:00

如下類別,請試著實作出它們的預設建構器(default constructor)和必要的拷貝控制成員。

(a)

class TreeNode

{

private:

std::string value;

int count;

TreeNode *left;

TreeNode *right;

};

(b)

class BinStrTree

{

private:

TreeNode *root;

};

TreeNode.h

#ifndef TREENODE_H

#define TREENODE_H

#include<string>

class TreeNode

{

public:

TreeNode() : count(0), left(nullptr), right(nullptr), user_cntr(new size_t(1)) {};//預設建構器(default constructor)

TreeNode(const std::string& s) :value(s),

count(s.size()), left(new TreeNode[s.size()]), right(left + s.size() - 1), user_cntr(new size_t(1)) {}//預設建構器

//拷貝建構器

TreeNode(const TreeNode& tn) :

value(tn.value), count(tn.count), left(tn.left), right(tn.right),user_cntr(tn.user_cntr) {

++* user_cntr;

}

~TreeNode() {

if (-- * user_cntr == 0) {

delete[]left;

//left = nullptr; right = nullptr;

delete user_cntr;

}

}

TreeNode& operator=(const TreeNode& rhs) {

++* rhs.user_cntr;

if (--*user_cntr==0)

{

delete[]left;

//left = nullptr; right = nullptr;

delete user_cntr;

}

value = rhs.value;

count = rhs.count;

left = rhs.left;

right = rhs.right;

user_cntr = rhs.user_cntr;

return *this;

}

private:

std::string value;

int count;

TreeNode* left;//由此想到動態陣列(動態配置多個物件);再由new[]想到要解構delete[]

TreeNode* right;

size_t* user_cntr;

};



#endif // !TREENODE_H

BinStrTree.h

#ifndef BINSTRTREE_H

#define BINSTRTREE

#include"TreeNode.h"

class BinStrTree

{

public:

//BinStrTree():root(nullptr){}//預設建構器

BinStrTree(const std::string& s=std::string()):root(new TreeNode(s)),user_cntr(new size_t(1)){}

BinStrTree(TreeNode* tnp):root(tnp),user_cntr(new size_t(1)){}

//拷貝建構器

BinStrTree(const BinStrTree& bst) :root(bst.root), user_cntr(bst.user_cntr){++* user_cntr;}

//拷貝指定運算子

BinStrTree& operator=(const BinStrTree & bst ){

++*bst.user_cntr;

if (--*user_cntr==0)

{

delete root;

delete user_cntr;

}

root = bst.root;

user_cntr = bst.user_cntr;

return *this;

}

//解構器

~BinStrTree() {

if (-- * user_cntr == 0) {

delete root;

delete user_cntr;

}

}

private:

TreeNode* root;

size_t* user_cntr;

};





#endif // !BINSTRTREE_H





.cpp

#include<string>

#include"TreeNode.h"

#include"BinStrTree.h"

using namespace std;

int main() {

TreeNode tn,tn1("孫守真"),tn2(string("阿彌陀佛"));

tn = tn1;

TreeNode tn3(tn2);

tn = tn2;

tn1 = tn3;

BinStrTree bst("妙音如來"),bst1(bst),bst2,bstp(new TreeNode(tn3));

bst2 = bst;

bstp = bst2;

}

https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise13_28a/prog1


13.3. Swap

1:20:39 3:44:14

會控管資源的類別除了會定義拷貝控制成員外,通常也會定義出一個叫做swap(§9.2.5,頁339)的介面函式。對於想要利用演算法(algorithm)來重排其型別元素的類別而言(§10.2.3,頁383),定義swap就更有需要了。一個有其專用的swap函式的類別,當演算法需要對調其型別的2個元素物件時,就能調動這個swap來應用。

只要類別有其自定義的swap函式,演算法要在這樣的類別物件上執行對調時,就會先呼叫這個類別它自己定義的swap來用;否則演算法就會改用程式庫定義的swap來應付。雖然,一般說來,我們並不知道swap函式到底是怎麼運作的,但我們也能夠想像得到swap的運算,必定需要1個拷貝和2個指定的運算。比如說,要對我們前面那個類值行為的HasPtr物件(§13.2.1,頁511)作swap(對調)的話,那麼應該是寫像這樣:

HasPtr temp = v1; //用一個暫存物件來作為v1的備份副本

v1 = v2; //將v2的值指定給v1

v2 = temp; //再將temp所備份的v1原有的值指定給v2

頁517

上述的程式碼會拷貝v1中的string值兩次,第一次是在用HasPtr的拷貝建構器來將v1的值拷貝到temp這個暫存物件中,第二次則是在利用指定運算子將temp的值拷貝給v2時。這個程式碼也會在將v2指定給v1時,把v2內的string值拷貝給v1。如我們先前所見,若將類值行為的HasPtr物件作拷貝的話,它會先用new配置出一個新的string,並將拷貝來源的HasPtr指向的string值拷貝給這個新配置出來的string用。

其實,這些資源的配置是沒有必要的。與其重新配置出新的string來作為對調的對象物件,還不如讓swap函式直接去置換拷貝雙方的指標來得好。也就是說,我們寧可教swap在置換二個HasPtr物件時作用如下:

string *temp = v1.ps; //將v1.ps的指標值存到暫存物件temp中

v1.ps = v2.ps; //先將v2.ps的指標值指定給v1.ps

v2.ps = temp; //再將temp所記錄下來的v1.ps指標值再指定給v2.ps

1:20:28 1:29:42 2:6:20 3:54:05

Writing Our Own swap Function

寫出我們自己定義的swap函式

我們是可以為我們的類別定義出它專用的swap函式來取代程式庫的版本。典型的模式如下:

class HasPtr

{

friend void swap(HasPtr &, HasPtr &);

//其他的成員一如§13.2.1(頁511)中所列

};

inline void swap(HasPtr &lhs, HasPtr &rhs)

{

using std::swap;

swap(lhs.ps, rhs.ps); //對指標作swap,而不是對string物件資料本身

swap(lhs.i, rhs.i); //對int資料成員做swap

}

1:35:52 2:7:53

我們先將swap函式宣告成HasPtr類別的朋友(friend,可見不是成員函式,而是介面函式!),這樣swap函式才有權去存取HasPtr中的私有(private)成員。因為,swap的存在是為了優化我們的程式碼,我們就把它定義為inline函式(§6.5.2,頁238)以避免函式呼叫時的額外負擔。swap函式的主體部分調用了swap函式來對給定物件的每個資料成員作swap運算。在此例中,我們是先將rhs和lhs的指標成員對調,再將它們的int成員調換過來。

注意:不像拷貝控制之必要,swap未必是用得著的。只是如果在對有配置資源需要的類別定義了它自己的swap函式的話,那麼定義自己的swap可能就是優化程式碼的一個重要部分。

1:45:33 2:9:44 3:59:05

swap Functions Should Call swap, Not std::swap

類別自定義的swap函式應該是要借用swap,而不是std::swap

以上程式碼有一個重要而易被忽略的關鍵部分,那就是自定義的swap函式要調用的,是swap,而不應該是std::swap。雖然在此例中並沒啥差別,但仍應留意!HasPtr類別(原文是function,疑筆誤。中文版照翻)的資料成員屬於內建型別的,而內建型別並沒有其型別專用的swap版本。因此在我們這個例子中,這些對swap的呼叫仍然會去調用到程式庫的std::swap。

In the HasPtr function, the data members have built-in types. There is no type-specific version of swap for the built-in types. In this case, these calls will invoke the library std::swap.

然只要類別的成員有它自己型別適用的swap函式,就不該去調用std::swap,而必須用它自己型別專用的swap。如有個Foo類別,有個成員叫做h,它的型別是HasPtr。

頁518

那麼如果我們對Foo沒有定義出它自己的swap,程式庫的swap就會被調用。一如我們之前所見到的那樣,程式庫的這個swap(就是std::swap),是會去拷貝出一個根據來源的HasPtr配置的string的副本出來,而如前所見,這是沒有必要的運算。(中文版竟然翻成這樣:如我們已經見到的,程式庫的swap會製作HasPtr所管理的string的不必要的拷貝。)

2:18:26 2:27:40 4:5:51

我們想要避免這樣不必要的拷貝運算,就須定義Foo型別自己的swap函式。然而如果我們把Foo的swap寫成這樣:

void swap(Foo &lhs, Foo &rhs)

{

//這是錯的!這樣一來,呼叫這個函式會調用到程式庫版本的swap,而不是HasPtr的

std::swap(lhs.h, rhs.h)

//對調其他Foo型別物件內的資料成員

}

這段程式碼仍然會被編譯與執行,但這樣一來,執行這段程式碼與之前那個直接用預設版本的swap(即程式庫的swap),並沒有什麼不同。問題出在「std::swap」這樣的寫法正是指明要調用程式庫版本的swap,而不是要用HasPtr的,所以結果才會如此。

正確撰寫Foo類別的swap函式的方式應該是像這樣:

void swap(Foo &lhs, Foo &rhs)

{

using std::swap;

swap(lhs.h, rhs.h) //這樣就會調用到HasPtr的swap,而不是程式庫的了

//對調其他Foo物件內的資料成員

}

2:30:28 3:24:23 4:8:44

所以,對swap的呼叫必須是非全名式的(unqualified)——意思就是要直接寫成「swap」,而不是「std::swap」。在§16.3(頁697)時我們會解釋這是為什麼。如果某一型別已有它專用的swap,那麼這個專用的swap決定是比定義在std的那個還要適用(match)。因此,如果有了型別專用的swap,那麼直接用「swap」作呼叫就會調用到這個型別專用的swap。而只要沒有與型別匹配的專用版swap,那麼只要在有效範疇內有用到using來宣告要使用swap,就會調用到std內的swap版本。

2:38:40 3:25:59 4:11:01

細心的讀者或許會問,為什麼在swap內的using宣告並沒有遮蔽掉HasPtr版本的swap呢?(§6.4.1,頁234)我們會到§18.2.3(頁798)再來解釋程式碼為什麼會這樣地運作。

2:42:03 3:27:27 4:11:57

Using swap in Assignment Operators 用swap函式來定義指定運算子

有自己定義swap的類別通常也會用這個swap來定義它們自己的指定運算子。這樣的運算子的定義通常是運用了一種所謂的copy and swap(拷貝並對調)的方式(technique)來完成的。這種方式就是利用右運算元值的副本來把左運算元的值給換掉:

//注意:rhs是以值的方式傳遞的,也就是說HasPtr的拷貝建構器會將右運算元的string值拷貝一份給rhs參數

HasPtr &HasPtr::operator=(HasPtr rhs)

{

//用區域變數(參數)rhs將左運算元的值給置換掉

swap(*this, rhs); //rhs現在指向的已是*this物件之前所佔用的記憶體位置

return *this; //離開=運算子這一函式範疇後,rhs會被摧毀,因而觸動HasPtr的解構器來刪除rhs內指標指向的動態配置物件。中文版翻譯不當:rhs被摧毁了,這會 delete rhs中的指標)

}

頁519

2:55:49 3:35:24 4:16:59

在這樣的指定運算子中,參數並不是一個參考,反而是將右運算元以傳值的方式來帶入。因此這個rhs會是拷貝右運算元後的一個獨立的副本,而對HasPtr物件進行拷貝,就會用new建置出一個原物件內string值的副本出來。

3:2:33 4:19:19

在這個指定運算子的主體部分,我們對swap進行了呼叫,這樣會將rhs和*this的資料成員進行互換。這個swap的呼叫會將原本在左運算元中的指標交給rhs,而將rhs中的交給*this。因此在swap執行後,在*this中的指標成員就會指向那個從右運算元新拷貝出來的string物件。

當這段程式碼結束時,rhs會被摧毀,也就會觸動HasPtr的解構器。這個解構器會將rhs現在指向的動態配置物件給清除掉,也就是會釋放原來左運算元指向的那個記憶體資源。

3:8:49 4:21:13

這項技術有意思的地方在於它不但了當處理了自指定/自拷貝的工作,又避免了例外情形發生的可能(exception safe,例外安全性)。因為在改動左運算元之前,它已經將右運算元拷貝好了,一如我們之前在對指定運算子為了能夠安全地自拷貝/自指定所做的那樣(§13.2.1,頁512);而其對於例外情形的掌控,也和之前的模式一樣。唯一可能會發生例外情形的地方,就只在於拷貝建構器內的new表述式(expression)了。即使有例外情形發生,也會在改動左運算元值前被引發。

要領(tip):用上copy and swap(拷貝對調)方式的指定運算子(也就是利用swap來定義的拷貝指定運算子,這樣的運算子)本能就可免於例外情形的發生、且可無虞地勝任自指定/自拷貝的工作。

練習13.29

4:26:20

請試著解釋一下為什麼在swap(HasPtr&,HasPtr&)中呼叫swap不會造成遞歸(recursion)迴圈?

呼叫自己的函式,無論是直接或間接,都被稱作遞迴函式(recursive function)。……一個遞迴函式必定要有一個執行路徑是不涉及遞迴呼叫的,否則函式就會「永遠」遞迴,這 表示函式會持續呼叫自身,直到程式堆疊(program stack)耗盡為止。(頁227)這種函式有時被描述為「含有一個遞迴迴圈(recursion loop)」。(頁228)

recursion loop (遞迴迴圈)描述省略了停止條件的遞迴函式,它會不斷呼叫自身,直到耗盡了程式堆疊為止。

Description of a recursive function that omits a stopping condition and which calls itself until exhasuting the program stack.

改中文版作文:對省了停止條件式、且會一直呼叫自己、直到應用程式的堆疊耗盡為止的遞歸函式的一種稱呼。

recursive function (遞迴函式)直接或間接呼叫自己的函式。(頁252)

因為函式名的「swap」與函式本體的「swap」同名而不同物,所以swap本體內swap並不是對其自己的呼叫。本體內的乃是std的,呼叫的是「std::swap」,因為ps和i都不是HasPtr型別,而是內建型別。

沒錄到的就看臉書直播第545集約1:30:00https://www.facebook.com/oscarsun72/videos/2666947576749693/

測試程式碼詳:

https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise13_29_HasPtr_swap/prog1

練習13.30

4:40:10 5:3:24

寫出並測試您自己的類值版本的HasPtr的swap函式。在此函式的定義中加入一段列印的指令(a print statement)來標記出swap被呼叫的時機。

HasPtr.h

#ifndef HASPTR_H

#define HASPTR_H

#include<string>

#include<iostream>

class HasPtr

{

friend void swap(HasPtr&, HasPtr&);

public:

HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0) {}

//拷貝建構器:每個HasPtr物件都有一個ps指標指向的那個string的副本

HasPtr(const HasPtr& p) : ps(new std::string(*p.ps)), i(p.i) {}

HasPtr& operator=(HasPtr);

~HasPtr() { delete ps; }



private:

std::string* ps;

int i;

};

inline void swap(HasPtr&lhs,HasPtr&rhs){//頁517

using std::swap;

swap(lhs.ps, rhs.ps);

swap(lhs.i, rhs.i);

std::cout << "swap here!感恩感恩 南無阿彌陀佛" << std::endl;

}

inline HasPtr& HasPtr::operator=(HasPtr rhs) {//頁518

swap(rhs, *this);

return *this;

}

#endif // !HASPTR_H

Foo.h

#ifndef FOO_H

#define FOO_H

#include<string>

#include"HasPtr.h"

class Foo

{

friend void swap(Foo&,Foo&);

public:

Foo(const HasPtr& hp) :hp(hp) {}

private:

HasPtr hp;

};

inline void swap(Foo& lhs, Foo& rhs) {

//using std::swap;

swap(lhs.hp, rhs.hp);

}



#endif // !FOO_H

.cpp

#include<string>

#include"HasPtr.h"

#include"Foo.h"

using namespace std;

int main() {

HasPtr hp,hp1("孫守真"),hp2(string("阿彌陀佛"));

hp = hp1;

Foo f(hp2),f1(hp1);

hp = hp2;

swap(f, f1);

swap(hp, hp1);

hp = hp;

}



5:16:55

練習13.31

將您在之前練習做出來的那個HasPtr類別給它定義一個<運算子,且定義一個由HasPtr元素所組成的vector。加入一些元素到這個vector,然後排序這個vector內的元素。請注意在排序這些元素時,HasPtr的swap被調用的時機。




6:1:37

.cpp

#include<string>

#include<vector>

#include<algorithm>

#include<iterator>

#include"HasPtr.h"

using namespace std;

int main() {

HasPtr hp,hp1("孫守真"),hp2(string("阿彌陀佛")),hp3("淨空老法師"),hp4("海賢老和尚"),hp5("阿彌陀佛");

vector<HasPtr>vhp{hp,hp1,hp2,hp3,hp4,hp5};

ostream_iterator<string>o(cout, ",");

for (HasPtr hp : vhp)

o++=hp.getStr() ;

std::cout<< std::endl;

sort(vhp.begin(), vhp.end());

for (HasPtr hp : vhp)

o++ = hp.getStr();

std::cout << std::endl;

}

HasPtr.h

#ifndef HASPTR_H

#define HASPTR_H

#include<string>

#include<iostream>

class HasPtr

{

friend void swap(HasPtr&, HasPtr&);

public:

HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0) {}

//拷貝建構器:每個HasPtr物件都有一個ps指標指向的那個string的副本

HasPtr(const HasPtr& p) : ps(new std::string(*p.ps)), i(p.i) {}

HasPtr& operator=(HasPtr);

bool operator<(const HasPtr&);

std::string& getStr();

~HasPtr() { delete ps; }



private:

std::string* ps;

int i;

};

inline void swap(HasPtr&lhs,HasPtr&rhs){//頁517

using std::swap;

swap(lhs.ps, rhs.ps);

swap(lhs.i, rhs.i);

std::cout << "swap here!感恩感恩 南無阿彌陀佛" << std::endl;

}

inline HasPtr& HasPtr::operator=(HasPtr rhs) {//頁518

swap(rhs, *this);

return *this;

}

inline bool HasPtr::operator<(const HasPtr& rhs) {

if ((*ps < *rhs.ps) && (i<=rhs.i))

{

return true;

}

return false;

}

inline std::string& HasPtr::getStr() {

return *ps;

}



#endif // !HASPTR_H



https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise13_31_HasPtr_swap_sort/prog1


練習13.32

類指標行為的HasPtr是否也會從定義了swap函式而得到方便?若是,有什麼便利之處;若否,又是為什麼?

6:17:9

定義一個swap函式對HasPtr的類指標版本有什麼好處嗎?如果有,好處是什麼?若無,為什麼呢?(中文版)

7:21:23

恐怕就是拷貝指定運算子定義時,對參考計數操作上,若仍須在撰寫程式碼時還要加上如同前面那樣自行寫定參考計數的計算,那就沒有方便太多了。

8:25:52

測試結果卻是OK的,和類值的是一樣便利!參考計數在swap及copy and swap後並無影響:

HasPtr.h

#ifndef HASPTR_H

#define HASPTR_H

#include<string>

//#include<iostream> //因為.cpp檔已include了,所以此可省略引用iostream

class HasPtr//類指標行為的類別

{

friend void swap(HasPtr&,HasPtr&);

public:

//預設建構器

HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0), user_cnt(new size_t(1)) { }

//拷貝建構器

HasPtr(const HasPtr& hp) :ps(hp.ps), i(0), user_cnt(hp.user_cnt) {

++* user_cnt;

std::cout << "拷貝建構器 阿彌陀佛" <<std::endl;

}

//拷貝指定運算子

//HasPtr& operator=(const HasPtr& hp) {

// ++* hp.user_cnt;

// //和解構器~HasPtr()做的是同一件事:

// //if (-- * user_cnt == 0)

// //{

// // delete ps;

// // delete user_cnt;

// //}//以上和解構器~HasPtr()做的是同一件事,只是解構器似乎不能被呼叫,故可另寫一個介面或成員函式來用

// destroy();

// if (*user_cnt != 0&&ps!=hp.ps) {//不是自拷貝/指定時才執行

// ps = hp.ps;

// i = hp.i;

// user_cnt = hp.user_cnt;

// }

// return *this;

//}

HasPtr& operator=(HasPtr);



~HasPtr() {

//if (-- * user_cnt == 0) {

// delete ps;

// delete user_cnt;

//}

destroy();

std::cout << "解構器 阿彌陀佛" << std::endl;

}

private:

std::string* ps;

int i;

size_t* user_cnt;

void destroy() {//此即所謂的工具函式(utility function)

if (-- * user_cnt == 0) {

delete ps;

delete user_cnt;

}

}

};



void swap(HasPtr& lhs,HasPtr& rhs) {

using std::swap;

swap(lhs.ps, rhs.ps);

swap(lhs.i, rhs.i);

swap(lhs.user_cnt, rhs.user_cnt);



}



//copy and swap(拷貝對調)

HasPtr& HasPtr::operator=(HasPtr hp) {

swap(*this, hp);

//因為引數hp在傳值時已經將參考計數器遞增了,故不用以下此行添足

//++* this->user_cnt;//參考計數只要顧到右運算元要遞增就好,左運算元遞增則交給被置換後的hp區域變數摧毀時呼叫的解構器來判斷

return *this;

}

#endif // !HASPTR_H



.cpp

#include<iostream>//因為這裡include,所以HasPtr.h才可以不用再次include它;如果這行省略,HasPtr.h編譯上就會出現錯誤

#include"HasPtr.h"

using namespace std;



int main() {

HasPtr hp("孫守真"), hp1("南無阿彌陀佛"), hp2("淨空老法師"), hp3("海賢老和尚"),hp4("常律老和尚"),hp5("白雲老禪師"),hp6;

hp6 = hp;

hp = hp1;

}

https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise13_32_HasPtr_pointerlike_swap/prog1




13.4. A Copy-Control Example 一個拷貝控制的實例

8:49:05 9:11:49

雖然拷貝控制成員對於會配置資源的類別而言多是有必要的,但管控資源並不是一個類別會需要定義拷貝控制成員的唯一理由。如有些類別會有一些記錄性的工作或其他操作都必須依靠拷貝控制成員來達成。

我們將引介兩個在郵件處理程式中可能會用到的類別來說明怎樣的類別會需要拷貝控制成員來完成相關的記錄操作。Message和Folder類別分別代表了電子郵件(或其他形式的)訊息,

頁520

以及這些訊息所在的目錄(資料夾)。每一個Message物件是可以同時出現在多個Folder物件中的,然而每個Message的內容卻只會有一份而已,不會在各個所在目錄(資料夾)中重複。也就是說,只要有一個Message物件所指向的內容有了變動,我們若從其他存放這個訊息的位置來打開這個訊息的話,這些變動也都會即時更新。

為了持續追蹤Message所在的目錄(資料夾),每個Message物件都會存放一個由指標組成的set容器,這些指標指向訊息所在的所有目錄。而每個Folder物件也會有一個由指標組成的set,這些指標則是指向它所對應到的Message物件。圖13.1演示了以上這樣的構思:



圖13.1.Message和Folder類別的設計藍圖(構想藍圖)

Figure 13.1. Message and Folder Class Design

9:18:55 9:43:49

Message類別則會提供save和remove的操作來在某個Folder新增或移除Message。在建置一個新的Message物件時,我們須提供這個Message的內容,但不用指定要存放的目錄。要指定哪個Message要放在哪個目錄(Folder)下,則是調用save函式來完成。

當拷貝一個Message物件時,拷貝的來源與目的會是各自獨立的Message物件,然而這二個物件都應該要有相同的由Folder指標組成的set(也就是其放置的位置也要一模一樣)。【中文版又把set誤翻了。set乃容器型別名稱也!但兩個Message都應該出現在同一組(set)的Folder中。】這樣一來,對Message進行拷貝就會同時拷貝訊息內容以及那個由Folder的指標組成的set容器。同時也必須將一個指向此新Message物件的指標給加入到這個新的Message物件所要放到的每個Folder物件中。

當刪除一則訊息,解構一個Message物件,就必須也連帶地將指向它的指標給清除乾淨。這些指標會分別存在原來這個訊息所在的Folder物件中。

當我們將一則訊息指定給另一則(即對Message物件作指定運算時),我們就必須將指定運算子的左運算元原有的資源給清除掉,然後再用右運算元的來加以取代。同時也必須去更新那個由指向Folder指標所組成的set容器的內容,一樣運算子左邊的要清除,而右邊的要新增。

回顧上述操作的流程,我們應不難理解,拷貝控制成員中的解構器與拷貝指定運算子都必須去照著Folder指向的物件來清除那個Message物件。而拷貝建構器與拷貝指定運算子也必須能夠去新增一個Message到它該放置的Folder中去。我們將定義一對私有的(private)工具函式(utility function)來完成上述工作。

9:57:53

養成好習慣:拷貝指定運算子通常做的工作,拷貝建構器與解構器也會做,因此,這些共通的工作,通常會把它作成一個工具函式(utility function)放到private區中。(也就如前面練習13.32我們自己寫的destroy成員函式)

10:8:10

頁521

Folder類別也需要相似的拷貝控制成員來從它所存放的Message物件來新增與移除它自己。

我們會把對Folder類別的設計與實作留給各位作練習。然而在這裡我們仍會先假設好Folder類別內有addMsg和remMsg這兩個成員函式來分別負責從某個Folder物件中所存放的set新增或移除Message物件。【英文版這裡的「set」也誤未改字型。這裡的set是Folder物件內的資料成員,用來存放有多少Messgae會放在這個Folder中的訊息。set的元素型別必是指向Message物件的指標。】

留言

熱門文章