《C++Primer》第十三章-複製控制-學習筆記(2)-析構函數

《C++Primer》第十三章-複製控制-學習筆記(2)

日誌:
1,2020-03-16 筆者提交文章的初版V1.0

作者按:
最近在學習C++ primer,初步打算把所學的記錄下來。

傳送門/推廣
《C++Primer》第二章-變量和基本類型-學習筆記(1)
《C++Primer》第三章-標準庫類型-學習筆記(1)
《C++Primer》第八章-標準 IO 庫-學習筆記(1)
《C++Primer》第十二章-類-學習筆記(1)

析構函數

構造函數(destructor)的一個用途是自動獲取資源。 例如,構造函數可以分配一個緩衝區或打開一個文件,在構造函數中分配了資源之後,需要一個對應操作自動回收或釋放資源。析構函數就是這樣的一個特殊函數,它可以完成所需的資源回收,作爲類構造函數的補充

何時調用析構函數

撤銷類對象時會自動調用析構函數:

// p points to default constructed object
Sales_item *p = new Sales_item;
{
// new scope
Sales_item item(*p); // copy constructor copies *p into item
delete p; // destructor called on object pointed to by p
} // exit local scope; destructor called on item

變量(如 item)在超出作用域時應該自動撤銷。 因此,當遇到右花括號時,將運行 item 的析構函數。
動態分配的對象只有在指向該對象的指針被刪除時才撤銷。 如果沒有刪除指向動態對象的指針,則不會運行該對象的析構函數,對象就一直存在,從而導致內存泄漏,而且,對象內部使用的任何資源也不會釋放。

當對象的引用或指針超出作用域時,不會運行析構函數。只有刪除指向動態分配對象的指針實際對象(而不是對象的引用)超出作用域時,纔會運行析構函數。

撤銷一個容器(不管是標準庫容器還是內置數組)時,也會運行容器中的類類型元素的析構函數:

{
Sales_item *p = new Sales_item[10]; // dynamically allocated
vector<Sales_item> vec(p, p + 10); // local object
// ...
delete [] p; // array is freed; destructor run on each element
} // vec goes out of scope; destructor run on each element  vec超出作用域撤銷了;

容器中的元素總是按逆序撤銷:首先撤銷下標爲 size() - 1 的元素,然後是下標爲 size() - 2 的元素……直到最後撤銷下標爲 [0] 的元素

何時編寫顯式析構函數

許多類不需要顯式析構函數,尤其是具有構造函數的類不一定需要定義自己的析構函數。僅在有些工作需要析構函數完成時,才需要析構函數。 析構函數通常用於釋放在構造函數或在對象生命期內獲取的資源。
如果類需要析構函數,則它也需要賦值操作符和複製構造函數,這是一個有用的經驗法則。 這個規則常稱爲三法則(Rule of Three),指的是如果需要析構函數,則需要所有這三個複製控制成員。
析構函數並不僅限於用來釋放資源。一般而言,析構函數可以執行任意操作,該操作是類設計者希望在該類對象的使用完畢之後執行的。

合成析構函數

與複製構造函數或賦值操作符不同編譯器總是會爲我們合成一個析構函數合成析構函數按對象創建時的逆序撤銷每個非 static 成員,因此,它按成員在類中聲明次序的逆序撤銷成員。對於類類型的每個成員,合成析構函數調用該成員的析構函數來撤銷對象。

撤銷內置類型成員或複合類型的成員沒什麼影響。尤其是,合成析構函數並不刪除指針成員所指向的對象。

如何編寫析構函數

Sales_item 類是類沒有分配資源因此不需要自己的析構函數的一個例子。分配了資源的類一般需要定義析構函數以釋放那些資源。
析構函數是個成員函數,它的名字是在類名字之前加上一個代字號(~),它沒有返回值,沒有形參。因爲不能指定任何形參,所以不能重載析構函數。雖然可以爲一個類定義多個構造函數,但只能提供一個析構函數,應用於類的所有對象。

析構函數與複製構造函數或賦值操作符之間的一個重要區別是,即使我們編寫了自己的析構函數,合成析構函數仍然運行
例如,可以爲 Sales_item類編寫如下的空析構函數:

class Sales_item {
public:
// empty; no work to do other than destroying the members,
// which happens automatically
~Sales_item() { }
// other members as before
};

撤銷 Sales_item 類型的對象時,將運行這個什麼也不做的析構函數,它執行完畢後,將運行合成析構函數(synthesized destructor)以撤銷類的成員。合成析構函數調用 string 析構函數來撤銷 string 成員,string 析構函數釋放了保存 isbn 的內存。units_sold 和 revenue 成員是內置類型,所以合成析構函數撤銷它們不需要做什麼。

消息處理示例

有些類爲了做一些工作需要對複製進行控制。 爲了給出這樣的例子,我們將概略定義兩個類,這兩個類可用於郵件處理應用程序。Message 類和 Folder 類分別表示電子郵件(或其他)消息和消息所出現的目錄,一個給定消息可以出現在多個目錄中。Message 上有 save 和 remove 操作,用於在指定 Folder 中保存或刪除該消息。

對每個 Message,我們並不是在每個 Folder 中都存放一個副本,而是使每個 Message 保存一個指針集(set),set 中的指針指向該 Message 所在的Folder。每個 Folder 也保存着一些指針,指向它所包含的 Message。將要實現的數據結構如圖 13.1 所示。
在這裏插入圖片描述

圖 13.1. Message 和 Folder 類設計

創建新的 Message 時,將指定消息的內容但不指定 Folder。調用 save 將Message 放入一個 Folder。
複製一個 Message 對象時,將複製原始消息的內容和 Folder 指針集,還必須給指向源 Message 的每個 Folder 增加一個指向該 Message 的指針。
將一個 Message 對象賦值給另一個,類似於複製一個 Message:賦值之後,內容和 Folder 集將是相同的。首先從左邊 Message 在賦值之前所處的 Folder中刪除該 Message。原來的 Message 去掉之後,再將右邊操作數的內容和Folders 集複製到左邊,還必須在這個 Folder 集中的每個 Folders 中增加一個指向左邊 Message 的指針。
撤銷一個 Message 對象時,必須更新指向該 Message 的每個 Folder。一旦去掉了 Message,指向該 Message 的指針將失效,所以必須從該 Message 的Folder 指針集的每個 Folder 中刪除這個指針。
查看這個操作列表,可以看到,析構函數和賦值操作符分擔了從保存給定Message 的 Folder 列表中刪除消息的工作。類似地,複製構造函數和賦值操作符分擔將一個 Message 加到給定 Folder 列表的工作。我們將定義一對private 實用函數完成這些任務。

Message 類
對於以上的設計,可以如下編寫 Message 類的部分代碼:

class Message {
public:
// folders is initialized to the empty set automatically
	Message(const std::string &str = ""):contents (str) { }
// copy control: we must manage pointers to this Message
// from the Folders pointed to by folders
	Message(const Message&);
	Message& operator=(const Message&);
	~Message();
// add/remove this Message from specified Folder's set of messages
	void save (Folder&);
	void remove(Folder&);
private:
	std::string contents; // actual message text
	std::set<Folder*> folders; // Folders that have this Message
// Utility functions used by copy constructor, assignment, and destructor:
// Add this Message to the Folders that point to the parameter
	void put_Msg_in_Folders(const std::set<Folder*>&);
// remove this Message from every Folder in folders
	void remove_Msg_from_Folders();
};

Message 類定義了兩個數據成員:contents 是一個保存實際消息的string,folders 是一個 set,包含指向該 Message 所在的 Folder 的指針。
構造函數接受單個 string 形參,表示消息的內容。構造函數將消息的副本保存在 contents 中,並(隱式)將 Folder 的 set 初始化爲空集。這個構造函數提供一個默認實參(爲空串),所以它也可以作爲默認構造函數。
實用函數提供由複製控制成員共享的行爲。put_Msg_in_Folders 函數將自身 Message 的一個副本添加到指向給定 Message 的各 Folder 中,這個函數執行完後,形參指向的每個 Folder 也將指向這個 Message。複製構造函數和賦值操作符都將使用這個函數。
remove_Msg_from_Folders 函數用於賦值操作符和析構函數,它從 folders成員的每個 Folder 中刪除指向這個 Message 的指針。

Message 類的複製控制

複製 Message 時,必須將新創建的 Message 添加到保存原 Message 的每個 Folder 中。這個工作超出了合成構造函數的能力範圍,所以我們必須定義自己的複製構造函數

Message::Message(const Message &m):
contents(m.contents), folders(m.folders)
{
// add this Message to each Folder that points to m
put_Msg_in_Folders(folders);
}

複製構造函數將用舊對象成員的副本初始化新對象的數據成員。除了這些初始化之外(合成複製構造函數可以完成這些初始化),還必須用 folders 進行迭代,將這個新的 Message 加到那個集的每個 Folder 中。複製構造函數使用put_Msg_in_Folder 函數完成這個工作。
編寫自己的複製構造函數時,必須顯式複製需要複製的任意成員。顯式定義的複製構造函數不會進行任何自動複製。
像其他任何構造函數一樣,如果沒有初始化某個類成員,則那個成員用該成員的默認構造函數初始化。複製構造函數中的默認初始化不會使用成員的複製構造函數。

put_Msg_in_Folders 成員

put_Msg_in_Folders 通過形參 rhs 的成員 folders 中的指針進行迭代。
這些指針表示指向 rhs 的每個 Folder,需要將指向這個 Message 的指針加到每個 Folder。
函數通過 rhs.folders 進行循環,調用命名爲 addMsg 的 Folder 成員來完成這個工作,addMsg 函數將指向該 Message 的指針加到 Folder 中。

// add this Message to Folders that point to rhs
void Message::put_Msg_in_Folders(const set<Folder*> &rhs)
{
for(std::set<Folder*>::const_iterator beg = rhs.begin();
beg != rhs.end(); ++beg)
(*beg)->addMsg(this); // *beg points to a Folder
}

這個函數中唯一複雜的部分是對 addMsg 的調用:

(*beg)->addMsg(this); // *beg points to a Folder

那個調用以 (*beg) 開關,它解除迭代器引用。解除迭代器引用將獲得一個指向 Folder 的指針。然後表達式對 Folder 指針應用箭頭操作符以執行addMsg 操作,將 this 傳給 addMsg,該指針指向我們想要添加到 Folder 中的Message。

Message 賦值操作符

賦值比複製構造函數更復雜。像複製構造函數一樣,賦值必須對 contents賦值並更新folders 使之與右操作數的 folders 相匹配。它還必須將該Message 加到指向 rhs 的每個 Folder 中,可以使用 put_Msg_in_Folders 函數完成賦值的這一部分工作。
在從 rhs 複製之前,必須首先從當前指向該 Message 的每個 Folder 中刪除它。我們需要通過 folders 進行迭代,從 folders 的每個 Folder 中刪除指向該 Message 的指針。命名爲 remove_Msg_from_Folders 的函數將完成這項工作。
對於完成實際工作的 remove_Msg_from_Folders 和 put_Msg_in_Folders,賦值操作符本身相當簡單:

Message& Message::operator=(const Message &rhs)
{
if (&rhs != this) {
remove_Msg_from_Folders(); // update existing Folders
contents = rhs.contents; // copy contents from rhs
folders = rhs.folders; // copy Folder pointers from rhs
// add this Message to each Folder in rhs
put_Msg_in_Folders(rhs.folders);
}
return *this;
}

賦值操作符首先檢查左右操作數是否相同。查看函數的後續部分可以清楚地看到進行這一檢查的原因。假定操作數是不同對象,調用remove_Msg_from_Folders 從 folders 成員的每個 Folder 中刪除該Message。一旦這項工作完成,必須將右操作數的 contents 和 folders 成員賦值給這個對象。最後,調用 put_Msg_in_Folders 將指向這個 Message 的指針添加至指向 rhs 的每個 Folder 中。
瞭解了 remove_Msg_from_Folders 的工作之後,我們來看看爲什麼賦值操作符首先要檢查對象是否不同。賦值時需刪除左操作數,並在撤銷左操作數的成員之後,將右操作數的成員賦值給左操作數的相應成員。如果對象是相同的,則撤銷左操作數的成員也將撤銷右操作數的成員!
即使對象賦值給自己,賦值操作符的正確工作也非常重要。保證這個行爲的通用方法是顯式檢查對自身的賦值。

remove_Msg_from_Folders 成員

除了調用 remMsg 從 folders 指向的每個 Folder 中刪除這個 Message之外,remove_Msg_from_Folders 函數的實現與 put_Msg_in_Folders 類似:

// remove this Message from corresponding Folders
void Message::remove_Msg_from_Folders()
{
// remove this message from corresponding folders
for(std::set<Folder*>::const_iterator beg =
folders.begin (); beg != folders.end (); ++beg)
(*beg)->remMsg(this); // *beg points to a Folder
}

Message 析構函數

剩下必須實現的複製控制函數是析構函數:

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

有了 remove_Msg_from_Folders 函數,編寫析構函數將非常簡單。我們調用 remove_Msg_from_Folders 函數清除 folders,系統自動調用 string 析構函數釋放 contents,自動調用 set 析構函數清除用於保存 folders 成員的內存,因此,Message 析構函數唯一要做的是調用 remove_Msg_from_Folders。
賦值操作符通常要做複製構造函數和析構函數也要完成的工作。在這種情況下,通用工作應在 private 實用函數中。

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