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



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

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

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

頁520

以及這些訊息所在的目錄(資料夾)。每一個Message物件可以同時「出現」在不同的Folder物件中,但Message的內容卻只會有一份,並不會重複存放在各個目錄(資料夾)中。也就是說,只要一個Message物件的內容有了變動,不管我們從哪個存放它的位置來打開這則訊息,這些變動都會即時反應出來。

為了持續追蹤Message所在的目錄(資料夾),每個Message物件內都會存放一個由指標組成的set容器,這些指標指向這個Message訊息所在的所有目錄。而每個Folder物件也會有一個由指標組成的set,這些指標則是指向它所對應到的Message物件。圖13.1演示了這樣的資料結構(有了資料結構才會有相應的類別設計;想要有怎麼樣的資料結構,就會有怎麼樣的類別設計):



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

Figure 13.1. Message and Folder Class Design

18:23

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

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

當刪除一則訊息——即準備解構一個Message物件時,就必須同時將指向它的指標給清除乾淨。這些指標會分別散置在原來這個訊息所在的Folder物件中。

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

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

41:03

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

頁521

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

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

The Message Class

有了以上的構思,我們就可以開始撰寫如下的Message類別:

class Message

{

friend class Folder;

public:

//會直接將資料成員folders這個set初始化為空的容器

explicit Message(const std::string &str = "") : contexts(str) {}

//拷貝控制管控指向這個Message物件的指標

Message(const Message &); //拷貝建構器

Message &operator=(const Message &); //拷貝指定運算子

~Message(); //解構器

//從這個Message物件所在的資料夾中新增/移除這個Message物件(新增/清除該Folder物件內的set容器內指向這則Message物件的指標元素)

void save(Folder &);

void remove(Folder &);

private:

std::string contents; //這個訊息物件Message實際的訊息內容

std::set<Folder *<folders; //這則訊息(Message物件)會存在的資料夾目錄

//供給拷貝控制成員(拷貝建構器、拷貝指定運算子、解構器)利用的工具函式(utility functions),來將這則訊息Message物件新增到Folder指標指向的位置

void add_to_Folders(const Message &);

//根據folders這個set容器中所指向的所有資料夾,來移除指向這則訊息的指標

void remove_from_Folders();

};

57:22

此Message類別定義了二個資料成員。contents是用來儲存訊息內容的,而folders則是用來存放那些有著這個Message的指標的資料夾的指標(其中元素型別為「*Folder」)。帶了一個string作引數的建構器會將這個引數拷貝到contents中,且直接(implicitly)將folders容器初始化為一個空的set。因為這個建構器只有一個引數、且有預設引數,故在用到它時這個引數是可以省略而不必帶入的,所以它也可以是Message的預設建構器(§7.5.1,頁290)。

1:0:48

The save and remove Members

成員函式save與remove

除了拷貝控制的成員,這個Message類別還有二個公開的成員函式。save的功能是將這個Message物件放到指定的資料夾目錄中,而remove則是將資料夾存放的這個Message物件的指標全部清除:

void Message::save(Folder &f)

{

folders.insert(&f); //用set類別的成員函式insert將一個資料夾目錄的指標加到這個Message所在的目錄清單set中 「&」係取址運算子

f.addMsg(this); //將這個Message的指標用Folder類別的addMsg成員函式來加到f這個資料夾目錄的訊息列表set中

}



void Message::remove(Folder &f)

{

folders.erase(&f); //從這個Message的目錄指標清單中移除f這個目錄指標。「&」係取址運算子。

f.remMsg(this); //從f目錄的訊息指標清單中移除這則訊息的指標

}

頁522

1:4:34

要儲存或移除一則訊息(Message物件)就需要更新這則訊息的folders資料成員的內容(值)。要新增這則訊息到某個資料夾目錄中時,我們就將指向這則訊息的指標存到它該存放到的資料夾目錄中。若要移除這則訊息,則從資料夾目錄所儲存的訊息指標清單中,移除這則訊息的指標。

這些操作都必須要能同步更新與此被操作的Message物件對應到的資料夾(即Folder物件)內容。Folder物件的更新動作是交由Folder類別的addMsg和remMsg這兩個成員函式來執行的;它們會在這個Message物件的指標所在的資料夾目錄(Folder物件)上新增加或移除掉這個指標。

1:7:53

Copy Control for the Message Class

Message類別的拷貝控制成員

對Message物件進行拷貝後,拷貝出來的副本也該在拷貝原本所在的資料夾目錄中出現。因此我們就必須要在原本的Message物件所儲存的set(所在資料夾目錄指標清單),一一找出其內所載的Folder指標它們所指向的Folder物件,來將指向這個副本的指標給新增進去。拷貝建構器和拷貝指定運算子都須用到這樣的運算,因此我們可以另外獨立定義出一個應用函式(工具函式,utility functions)來專責處理這兩個拷貝控制成員都要用到的運算:

//將這個Message的副本加到其原本訊息所在的資料夾目錄中

void Message::add_to_Folders(const Message &m)

{

for (auto f : m.folders) //對m所在的資料夾目錄作逐一地巡覽;「auto」型別為「*Folder」。

f->addMsg(this); //將這個副本的訊息指標新增到原訊息m所在的資料夾目錄中

}

這裡我們在m的folders成員內的每個Folder指標上,用箭號運算子來調用Folder的addMsg成員函式。這個函式會把這則副本訊息的指標this新增到這個Folder的訊息指標列表容器set(這個資料成員)中。





1:15:32

Message的拷貝建構器會拷貝其Message物件內的所有資料成員:

Message::Message(const Message &m) : contemts(m.contents), folders(m.folders)

{

add_to_Folders(m); //將這則拷貝出來的Message加到它要存放的資料夾目錄

}

拷貝建構器也會調用add_to_Folders這個工具函式(utility functions)來將這個新拷貝出來的訊息指標,存放到其拷貝來源m所存放的資料夾目錄中。

1:18:24

The Message Destructor

Message類別的解構器

在刪除一則訊息時(即解構一個Message物件時),也必須同時將它所在的資料夾目錄,所存有的所有指向這則訊息的指標都給清除。而拷貝指定運算子也需用到這樣的操作,因此我們也可以另外再定義出一個通用的函式(即工具函式,utility functions)來執行這樣共通的操作:

void Message::remove_from_Folders()

{

for (auto f : folders) //對存在folders這個set型別的資料成員中的每個「*Folder」(Folder指標)

f->remMsg(this); //將「*f」(解參考f這個Folder指標)內存放的這個Message的指標(即「this」指標)給清除掉,就表示不再有資料夾目錄會再有指向這則訊息(這個Message物件)的指標了

folders.clear(); //將這個folders set容器的資料成員的內容物也清除掉;其實解構器在解構資料成員時效力就等同於此行了,對於建構器而言,此行是不必要的,甚至是多餘的,然而這個工具函式是要與拷貝指定運算子共用的,拷貝指定運算子就需要有這一行才能將原有的指標清除,因為它並不需要解構folders這個資料成員。

}

頁523

這個remove_from_Folders實作起來很像add_to_Folders,因為它們都只是利用了folders這個資料成員而已,且也調用到了Folder類別的成員函式,只不過它不是用Folder類別的addMsg成員函式,而是用remMsg來移除現在這個Message物件的指標。

1:30:21

有了remove_from_Folders這樣的工具函式,Message的解構器寫起來就精簡多了:

Message::~Message()

{

remove_from_Folders();

}

引用了remove_from_Folders就能確保不再有資料夾目錄還會有指向這個被摧毀的Message物件的指標。而對於另外兩個資料成員,編譯器自會調用string類別的解構器來釋放contents,而用set類別的來清除folders所佔用的資源。

1:31:52

Message Copy-Assignment Operator

Message類別的拷貝指定運算子

也和大多數的指定運算子一樣,Message類別的拷貝指定運算子也必須要做到拷貝建構器與解構器所要做的事。【英文版Message誤作Folder】而在定義這樣的指定運算子時,也要確保它能在左、右兩邊運算元是同一物件時,還能正常運作。

為了確保自拷貝與自指定能正常運行,我們會先移除左運算元上的folders成員所有的Folder指標,然後才將右運算元中的folders成員指定給它。

1:33:46

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

{

//為了自指定自拷貝的安全考量,我們會在指定給左運算元新的指標前,先移除左運算元上原有的指標。

remove_from_Folders(); //更新現有的Folder物件,清除它們儲存的指向左運算元的指標。

contents = rhs.contents; //從rhs(右運算元)將contents(訊息內容)拷貝過來;利用的是string的指定運算子

folders = rhs.folders; //從rhs將這個訊息對應到的所有Folder的指標給拷貝過來;利用的是set的指定運算子

add_to_Folders(rhs); //將這則訊息加到所有它要在的資料夾中。「rhs」應也可以用「*this」來代入。因為此時rhs和*this的folders(經過上一行後)已然相同了。

return *this;

}

一旦左、右雙方的運算元是同一個物件,那麼它們就會有相同的記憶體位址(應是指this指標,因為remove_from_Folders會用這個this作為引數去清除)。萬一我們在add_to_folders之後才呼叫remove_from_folders,我們就會誤將所有資料夾中儲存這則訊息的指標給移除了。

Had we called remove_from_folders after calling add_to_folders, we would have removed this Message from all of its corresponding Folders.

【中文版翻譯有誤:假設我們在呼叫add_to_Folders 之後才呼叫remove_from_Folders,我們就已經從它對應的所有Folder移除了這個Message。】

1:44:57

A swap Function for Message

Message類別專用的swap函式

Message的資料成員型別,string和set這兩個類別,程式庫都已經定義了它們可用的swap(§9.2.5,頁339),因此,若Message也能定義自己的swap,想必會有一定的方便;因為有了這個swap,就能省去對其資料成員(contents和folders)多做些不必要的拷貝運算。

只是這個swap函式也要能處理那些指向經過swap後的Message的Folder指標。也就是說在經過swap後,如swap(m1,m2),原來有著指向m1的Folder指標此時就必須改為指向m2了,反之亦然。

頁524

我們會對folders成員處理兩次,用這個方式來處理這些Folder指標。第一次會從相關的Folder物件中移除其Message指標,接著調用swap來調換其中的資料成員。然後再第二次處理folders,【英文版誤作「folders(folderS)」,中文版卻不誤,且翻得不錯!】,這次就是將指標加到已經置換後的Message物件中。(也就是第一次先清空原有的指標,對調後,再加入該有的指標)

We’ll manage the Folder pointers by making two passes through each of the folders members. The first pass will remove the Messages from their respective Folders. We’ll next call swap to swap the data members. We’ll make the second pass through folders this time adding pointers to the swapped Messages:

我們會對每個folders成員做兩回處理,以管理那些Folder指標。第一回會從它們分別對應的Folder移除Message。接著我們會呼叫swap來對調資料成員。這次我們會對folders進行第二回處理,新增指標到調換過的Message:(中文版)

pass在這裡是翻成「遍」

making two passes through:通過做兩遍

1:48:41

void swap(Message &lhs, Message &rhs)

{

using std::swap; //雖然在此例中不必用到這個(因為string、set這兩個資料成員用到的類別已有定義自己專用的swap了,就不必調用到程式庫定義的swap),但還是養成好習慣,把它寫出來

//從這兩個要對調的Message物件它們本來存在的資料夾目錄中移除指向它們的指標

for (auto f : lhs.folders)

f->remMsg(&lhs); //&為取址運算子,下同。

for (auto f : rhs.folders)

f->remMsg(&rhs);

//對調資料成員contents和folders(即由Folder指標組成的set容器)

swap(lhs.folders, rhs.folders); // 用set容器所定義的swap來對調,即swap(set&,set&)

swap(lhs.contents, rhs.contents); //swap(string&,string&)利用程式庫對string類別定義的swap來做contents的對調

//在這兩個對調的Message該放到的資料夾目錄中新增指向它們的指標

for (auto f : lhs.folders)

f->addMsg(&lhs);

for (auto f : rhs.folders)

f->addMsg(&rhs);

}

練習13.33

1:58:00

Message類別的save、remove這兩個成員函式中的參數型別為什麼不是Folder、或const Folder&,而是對Folder的參考「Folder&」?

因為要傳址,且要更動,不能是const。

void Message::save(Folder &f)

{

folders.insert(&f); //「&」係取址運算子;若f型別是Folder,那麼就是引數的拷貝副本,對其下取址運算子,取得的必然不是原本的位址,而是此副本的位址

f.addMsg(this); //既然f是要被add,也就是會被變動,當然就不能是對常值的參考,也就是不能是「const Folder&」型別了。對常值的參考是指不能透過此參考來更動其所參考到的物件。這裡既要更動,當然就不能用對常值的參考了。

}

void Message::remove(Folder &f)

{

folders.erase(&f); //「&」係取址運算子;若f型別是Folder,那麼就是引數的拷貝副本,對其下取址運算子,取得的必然不是原本的位址,而是此副本的位址

f.remMsg(this); //同save,只不過不是add,而是rem(remove)

}

2:8:36 2:31:30

留言

熱門文章