C++自修入門實境秀、C++ Primer 5版研讀秀 102/ ~第13章 拷貝控制13.1.4. The Rule of ThreeFive...
練習12.33
第102集開始
在15章我們會再擴展我們的查詢系統,在QueryResult類別中加入一些額外的成員。試著先加入begin和end這兩個成員函式以取得查詢結果中儲存行號的set的首尾二個迭代器。還有一個叫做get_file的成員函式,它會回傳一個shared_ptr,指向QueryResult物件內的那個檔案(file)。
在第15章中,我們會擴充我們的查詢系統,並且會需要在QueryResult類別中添加一些額外的成員。新增名為begin和end成員,它們會回傳迭代器指向一個給定的查詢所回傳的行號set中,以及一個名為get_file的成員,回傳一個shared_ptr指向QueryResult物件中的檔案。(中文版)
.cpp
這裡只錄改過的部分:
cout <<"the context of the first line in the file is \""<< *qr.get_file()->begin() << "\""<<endl;
cout << "the FIRST line is line " << *qr.begin() << endl;
cout <<"the LAST line is line " <<*--qr.end() << endl;
餘詳:https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise12_33/prog1
QueryResult.h
class QueryResult
{
public:
QueryResult(shared_ptr<pair<string, set<size_t>>>sp_key) :pair_str_set(sp_key) { found = false; }
QueryResult(shared_ptr<vector<string>>, shared_ptr<pair<string, set<size_t>>>);
~QueryResult();
void print();
set<size_t>::iterator begin();
set<size_t>::iterator end();
const shared_ptr<vector<string>>&get_file()const;
private:
shared_ptr<vector<string>>vs;
shared_ptr<pair<string, set<size_t>>>pair_str_set;
bool found;
};
34:00新增weak_ptr以作回傳shared_ptr前的檢查
inline set<size_t>::iterator QueryResult::begin(){
return pair_str_set->second.begin();
}
inline set<size_t>::iterator QueryResult::end()
{
return pair_str_set->second.end();
}
inline const shared_ptr<vector<string>>& QueryResult::get_file()const
{
weak_ptr<vector<string>>w(vs);
if (!w.expired())
return vs;
else
throw runtime_error("不是有效的檔案");
}
本章總結
24:40 38:00沒錄到看臉書直播第527集https://www.facebook.com/oscarsun72/videos/2625200847591033/
在C++語言中,是由new的表述式(expression)來配置動態記憶體的,而由delete來釋放它。程式庫還定義了allocator類別以便用來配置動態記憶體(dynamic memory)區,將配置與建構(初始化)的工作來分開。
只要用到了動態記憶體區,都要記得在不再用到它的時候要將曾經配置出來的記憶體給釋放掉。如何在適當的時機下釋放動態配置的記憶體,往往是設計程式時最令人頭痛的問題。一般要不是忘了、或沒能釋放配置出來的記憶體資源,就是在還有指標指向某記憶體區時,卻先行釋放了它。因此新的C++程式庫就定義了智慧指標(shared_ptr、unique_ptr、weak_ptr)來讓動態記憶體的管理更為便宜。智慧指標會在適當的時機自動釋放它所指向的記憶體;當不再有其他非獨立建構出來的智慧指標指向該記憶體位置時,它就釋放該處的記憶體。因此新的C++程式碼都應該改用智慧指標來管理動態配置記憶體才好。
學過的詞彙
1:23:20
allocator
一種專司配置出原生而未經建構初始化記憶體區的程式庫類別(library class)。
懸置指標(dangling pointer)
一個指向曾有物件而已經不再的記憶體區的指標。因為懸置指標而造成的程式錯誤是很難加以除錯的。
delete
用來釋放由new配置出來的記憶體資源。「delete p」這樣的述句是表示要釋放的是一個物件,而「delete [] p」則是要釋放由p所指向的動態陣列。運算元p可以是空指標null,或一個有效指向由new配置出來的記憶體區域的指標。
刪除器(deleter)
用來傳入給智慧指標作為它用來刪除它所指向的物件的函式,以取代預設用到的delete運算子。
解構器(destructor)
當類別物件被delete或死亡時,用來清除其所佔用資源的一種成員函式。
動態配置的(dynamically allocated)
在free store記憶體區中配置出來的物件。在此記憶體區中配置出來的物件會一直存在到明確釋放它們、或整個應用程式結束為止。
自由存放區(free store)
用來存放應用程式動態配置出來物件的記憶體區域(memory pool)。
讓程式用來存放動態配置物件的記憶體集區(memory pool)。(中文版)
heap
是自由存放區的一個別名。
new
1:4:40 1:29:30在自由存放區(free store)上配置記憶體。「new T」就是配置且建構一個T型別的物件出來,並且回傳一個指向這個物件的指標。如果T是陣列型別,new回傳的就會是指向該陣列第一個元素的指標。同樣地,「new [n] T」是配置n個T型別的物件,然後回傳一個指向其第一個物件的指標。預設情況下,new出來的物件是經由預設初始化的,我們也可以提供自己的初始器來初始化剛建置出來的物件。
placement new (放置型的new)
一種new的表達式(form、語法),在new後面接了一對圓括弧以指定一個額外的引數。例如「new (nothrow) int」就表示new在配置int時,若發生了錯誤,決定不要丟出例外情形而中止程式。
參考計數(reference count)
用來表示有多少智慧指標指向了同一個物件。智慧指標藉以判斷安全刪除其所指向物件的適當時機。
shared_ptr
一種對於其所指向物件之所有權乃共享的智慧指標。當最後一個shared_ptr死掉時,它會自動將其所指的物件給清除。
智慧指標(smart pointer)
一種程式庫的型別(library type),它的行為如同一般指標,但是具有檢測機制,來測試它目前是否是個有效的指標。這種型別的物件會在適當的時機下,清除它所指向的不必要的記憶體資源。
unique_ptr
獨佔性的智慧指標。當一個有效的unique_ptr被摧毀時,它所指向的物件也會隨之清除。unique_ptr並不接受直接地拷貝或指定,因為他是「唯一、獨一無二」的。
weak_ptr
一種指向由shared_ptr管控物件的智慧指標。它並不參與該shared_ptr參考計數的計算,所以當shared_ptr要刪除其所指向的物件時,並不會去管還有沒有weak_ptr在指向著該物件。
C++ Primer Fith Edition第三篇重譯撰 中文博士孫守真任真甫學
Part III: Tools for Class Authors 第三部分 寫作類別者使用的工具
頁493
本部分/篇目錄
第13章 拷貝控制
第14章 重載(overload)的運算與轉型(型別轉換)
第15章 物件導向程式設計
第16章 模板(template)和泛型(generic)程式設計
1:58:20 沒錄到的部分就請看臉書直播第528集https://www.facebook.com/oscarsun72/videos/2625777540866697/
第15章涵蓋繼承和動態繫結(dynamic binding)。連同資料抽象化,繼承和動態繫結都是 物件導向程式設計的基礎之一。繼承讓我們更容易定義相關的型別,而動態繫結讓我們撰寫 獨立於型別的程式碼,能夠忽略透過繼承產生關聯的型別之間的差異。
第16章涵蓋函式和類別模板。模板讓我們撰寫獨立於型別的泛型類別和函式。新標準引進了 數個與模板有關的新功能:variadic templates (參數可變的模板)、模板型別別名,以及控 制實體化(instantiation)的新方法。
撰寫我們自己的物件導向或泛用型別需要對C++有相當良好的理解。幸好,我們不需要了解 如何建置它們的細節就能運用物件導向和泛用型別。舉例來說,標準程式庫廣泛使用我們會 在第15章和16章中研究的機能,而且我們已經用過程式庫型別和演算法,也不需要知道它 們是如何實作的。
因此,讀者應該了解第三篇涵蓋相當進階的主題。撰寫模板或物件導向類別需要對C++的基 礎有很好的理解,而且對於如何定義較為基本的類別掌握良好。
類別是C++語言的核心概念。從第7章開始,我們就談論了類別要如何去定義。那章談論的都是在使用任何類別時(包括這裡的物件導向與泛型類別等)需要知道的基本概念,諸如:類別的範疇(class scope)、資料的遮蔽(data hiding,資料隱藏),還有建構器(constructor)。那裡也介紹了許多重要的類別屬性及其功能,諸如:成員函式、隱含的this指標,類別與類別及類別與介面間的朋友friend關係(friends),還有const常值(唯讀)、static靜態和mutable成員。在本部分/篇中,我們將更進一步來探討有關類別的拷貝控制、重載運算子、繼承關係(inheritance)以及模板(template)。
C++的類別定義了建構器,來決定當其類別物件被建置時,會發生什麼事。類別也能決定其物件在被拷貝、指定、移動、摧毀時,分別該發生什麼事。因此,C++實有別於其他許多的程式語言;因為它們並不允許寫作類別者執行諸如此類的運算與操作。第13章就會討論這些運算與操作。在其中也會提到在新標準下的2個重要觀念:右值參考(rvalue references)和移動運算(move operations)。
第14章則專門探討運算子的重載問題。具有這樣的特性,才使得類別得以利用內建的運算子(built-in operator)來定義自己的版本,而內建的運算子也才能使用在以此類別作為運算元的情境中。C++就是利用運算子的重載來讓我們得以在對新類別物件進行運算時,也能像在對內建型別的物件一樣地直觀、好用。
運算子重載是C++讓我們創建出跟內建型別一樣直覺易用的新類別的方式之一。(中文版)
頁494
在這些類別可以重載的運算子中,有一種叫做函式呼叫運算子(function call operator,英文版function誤作funtion,中文版照copy)。我們可以對這樣的類別物件作呼叫,就好像它們是函式一樣。我們也會談到在新的程式庫機制中,能夠以更方便且統一的方式來使用不同型別的可呼叫物件(callable object)。
我們也會看看新的程式庫機能,它們能讓我們更容易以一致的方式使用不同型別的可呼叫物件。(中文版)
這一章的最後則會談到另一種特殊的類別成員函式,它叫做型別轉換運算子(轉換運算子,conversion operators)。這些運算子定義了對於其類別物件的隱含轉型。編譯器則會在同樣的情境與同樣的理由來套用這些轉型,一如它對於內建型別(built-in type)間的轉型所做的那樣。
這些運算子定義了從類別型別的物件的隱含轉換。編譯器會在相同的情境中,基於相同的理由,套用這些運算,就像內建型別間的轉換一樣。(中文版)
這部分/篇的最後兩章則會談到C++與物件導向、泛型程式設計的關係。
第15章談到的是繼承關係和動態繫結(dynamic binding);一如資料抽象化(data abstraction),繼承和動態繫結也是物件導向程式設計的根本基礎。繼承讓我們更容易去定義一些相關聯的類別,而動態繫結則讓我們能夠寫出可忽略有著繼承關係的類別間的不同,而獨立於其型別之外的程式碼。
而動態繫結讓我們撰寫獨立於型別的程式碼,能夠忽略透過繼承產生關聯的型別之間的差異。(中文版)
第16章將談論的是函式與類別的模板(template)。模板讓我們得以寫出獨立於特定型別之外的泛型類別和函式。新標準也引入了一些新的與模板相關的功能,諸如:參數可變的模板(variadic templates),模板型別別名(template type aliases),還有一個新的方式來掌控實例化的過程。(and new ways to control instantiation. 中文版:控制實體化(instantiation)的新方法。)
要想寫出我們自己的物件導向或泛型(泛用型別),是需要有相當可觀的C++知識基礎的。幸運的是,我們可以直接使用物件導向與泛型(泛用型別),而不用去管組建它們的細節為何。怎麼說呢?如標準程式庫本身就大量應用了15、16兩章所談到的機制,而我們也已經用過的程式庫型別及演算法,也都沒有必要去瞭解它們是如何實作出來的。
標準程式庫廣泛使用我們會在第15章和16章中研究的機能,而且我們已經用過程式庫型別和演算法,也不需要知道它們是如何實作的。(中文版)
因此,讀者應當知曉本第3部分有著相當進階的知識。若要寫作模板或物件導向的類別,是需要對C++的基礎知識有相當的熟稔,且對於定義一般的類別也已具相當熟悉的程度才能駕馭的。
C++ Primer Fith Edition第三篇第13章重譯撰 中文博士孫守真任真甫學
頁495
Chapter 13. Copy Control 第13章 拷貝控制
本章目錄
2:45:20 3:4:50
13.1 拷貝、指定和摧毀
13.2 拷貝控制和資源管理
13.3 置換(swap,對調)
13.4 拷貝控制的實例
13.5 管控動態記憶體(dynamic memory)的類別
13.6 物件的移動(moving objects)
本章總結
學過的詞彙(觀念)
在前面第7章中我們已經知道類別的功能是定義出一個新的型別出來,且依此型別而生的物件,其可進行的運算與操作會是什麼。類別也會定義建構器,來掌控在依其類別定義而生的物件在建置時會發生的事宜。
在本章中,我們會學到類別是如何管控它的物件在經過拷貝、指定與移動或摧毀時會/能發生事。【可見所謂的「拷貝控制」,找對主詞,誰在控制?類別也!】類別是透過一些特殊的成員函式來進行這樣的管控的,諸如:拷貝建構器、移動建構器、拷貝指定運算子、移動指定運算子,還有解構器。
頁496
當定義一個類別時,我們都會在有意無意間(explicitly or implicitly)指定當這個類別的物件在經過拷貝、移動、指定與摧毀時會發生什麼事。類別是經由以下五個特殊的成員函式來做到這些管控的:拷貝建構器、拷貝指定運算子、移動建構器、移動指定運算子和解構器(destructor)。拷貝與移動建構器定義了在經由同型別的物件來初始化某一物件時會發生什麼事。而拷貝指定與移動指定運算子則定義了在將一同型別的物件,指定給另一個物件時,會發生的事。解構器(destructor)則定義了當其類別的物件死亡時會發生什麼事。以上的這些操作,可統稱為拷貝控制。
3:28:20
如果一個類別並沒有定義所有的拷貝控制成員,那麼編譯器就會自動代勞,補上未定義的部分。【可見不管有沒有手動定義,這五個拷貝控制的操作與運算是一定要有的、不可或缺的。】3:29:50 3:38:10也就是這樣,許多的類別在定義時才會忽略了這個部分(§7.1.5,頁267)。然而若一味地仰賴編譯器代勞,那麼有些時候可能就會發生意想不到的嚴重後果!通常實作拷貝控制最困難的部分就在於如何正確地掌握定義它們的時機。
經常,實作拷貝控制運算最困難的部分就是判斷出我們何時需要定義它們。(中文版)
警告
拷貝控制是定義C++類別時最基本的任務。C++程式設計的新手往往會茫然於在類別物件被拷貝、移動、指定或解構(摧毀)時,該怎麼去定義適當的行為。這種困惑還來自於如果我們未能明確定義自己的拷貝控制,那麼編譯器就會雞婆地自動代我們來做,然而它所編譯出來的操作或行為,卻未必符合我們在定義此類別時所希望的那樣。
拷貝控制是定義任何C++類別時都不可或缺的部分。C++的程式設計師新手經常會對「需要定義物件被拷貝、移動、指定或摧毀時會發生什麼事」這件事感到困惑。這個困惑還會因為編譯器會在我們沒有明確定義這些運算時為我們定義它們而加重,雖然編譯器定義的版本之行為可能不是我們所預期的。(中文版)
13.1. Copy, Assign, and Destroy 拷貝、指定與摧毀
我們先來看最基本的運算與操作,那就是拷貝建構器、拷貝指定運算子和解構器。到了§13.6(頁531)我們才會談到由新標準引進的移動運算。
13.1.1. The Copy Constructor 拷貝建構器
只要一個建構器符合以下的特質,它就是一個拷貝建構器:
1.它的第一個參數就是對它型別的參考
2.任何其他的參數都有預設值
3:54:00
class Foo
{
public:
Foo(); //這是預設建構器(default constructor)
Foo(const Foo &); //此即拷貝建構器
//……
};
4:5:10
拷貝建構器的第一個參數必是對其類別的參考,原因我們等下解釋。且這個參數通常都是一個對常值const的參考;然而我們也是可以定義出一個拷貝建構器它的第一個參數並不是對常值的參考。而在許多的情境下,拷貝建構器是默認被調用的,因此它也不應是個explicit的建構器(§7.5.4,頁296)。
頁497
4:6:30
The Synthesized Copy Constructor 由編譯器湊合的拷貝建構器
4:22:40
當我們對自己的類別沒有定義出一個拷貝建構器,那麼編譯器就會為我們湊合一個。不像編譯器為我們在沒有建構器的情況下湊合出一個預設建構器(§7.1.4,頁262),即使我們已經定義了其他的建構器,編譯器依然會為我們湊合出拷貝建構器,只要我們沒有定義自己的版本。
在§13.1.6(頁508)我們會看到,編譯器為有些類別湊合出來的拷貝建構器卻會讓其型別物件無法進行拷貝。此外,湊合的拷貝建構器則是按其引數中的成員、依序來拷貝到被建置出來的新物件。(§7.1.5,頁267)編譯器會一個個將其中非靜態的成員由來源的引數物件拷貝到目的地的甫建置出來的物件。
中文版這裡靜態static錯成了const:
編譯器會從給定的物 件依序拷貝每個非const成員到正在建立的物件中。(中文版)
The compiler copies each nonstatic member in turn from the given object into the one being created.
4:33:40
而每個成員的型別屬性就決定了它們要如何被拷貝:如果是類別型別的成員,就會藉由該類別的拷貝建構器來進行拷貝。如果是內建型別(built-in type)的成員,那麼就會直接進行拷貝。雖然沒有辦法直接拷貝陣列(§3.5.1,頁114),但編譯器湊合出來的拷貝建構器則會利用其陣列元素的型別來對其元素進行一個個地拷貝。如果元素的型別是類別,那麼就藉由元素型別所定義的拷貝建構器來進行拷貝。
4:37:26 沒錄到的部分請看臉書直播第529集https://www.facebook.com/oscarsun72/videos/2627278787383239/
比如說,編譯器就會我們的Sales_data類別湊合出這樣的拷貝建構器:
class Sales_data
{
public:
//其他成員與建構器一如之前
//宣告(模擬)一個類似編譯器為我們湊合的拷貝建構器
Sales_data(const Sales_data &);
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
};
//類似編譯器為Sales_data湊合出的拷貝建構器
Sales_date::Sales_data(const Sales_data &orig) :
bookNo(orig.bookNo), //這個拷貝初始化是用了string的拷貝機制(即string類別的拷貝建構器)
units_sold(orig.units_sold), //這是用了int的拷貝機制來拷貝oring.units_sold到目的物件上
revenue(orig.revenue) //用double的拷貝機制來將orig.revenue拷貝到目的物件上
{} //空的函式(即拷貝建構器)本體【可見這個模擬編譯器的拷貝建構器只有在建構器初始器串列上作拷貝初始化(copy initialize),而沒再做額外的事情】
4:42:00
拷貝初始化(copy initialization)
是時候來徹底瞭解直接初始化(direct initialization)和拷貝初始化(copy initialization,§3.2.1,頁84)有什麼不同了:
string dots(10, '.'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷貝初始化
string null_book = "9-999-99999-9"; //拷貝初始化
string nines = string(100, '9'); //拷貝初始化
4:48:50
當我們在做直接初始化時,我們是叫編譯器依一般函式匹配(function matching,§6.4,頁233)的方式,依我們所提供的引數來調用最合適的建構器。5:19:00而拷貝初始化則是要編譯器將右手邊的運算元拷貝給甫/已建置出來的物件;而若有需要,就進行型別上的轉換(§7.5.4,頁294)。
頁498
5:19:38
拷貝初始化通常用的是拷貝建構器,然而就如同我們會在§13.6.2(頁534)所見,一旦某個類別具有移動建構器,那麼拷貝初始化「有時候」(不是總是)就會用那個移動建構器,而不會用上拷貝建構器了。然而現在我們該知道的是:何時會用到拷貝初始化,而在拷貝初始化時,到底是要用移動建構器還是拷貝建構器。
要用到拷貝初始化的時機或情境是:
1.當定義一變數並使用「=」指定運算子將之初始化時
2.當將一個物件作為引數,傳遞給非參考型別的參數時(只要參數的型別不是參考,那麼傳遞引數給它的時候,就在進行拷貝初始化)
3.由函式回傳一個非參考的回傳型別時
4.利用串列初始化(list initialization,brace initialize。中文版:「以大括號初始化」)來初始化陣列內的元素或者是初始化彙總類別(aggregate class)的成員時
5.有些類別也會利用拷貝初始化來初始化它們的物件。比如說程式庫容器會在我們初始化該容器時用拷貝初始化來初始化它們的元素。或者當我們調用insert或push成員函式(§9.3.1,頁342)時,實際上也是在進行拷貝初始化,因為這二者是利用「拷貝」的方式來加入元素的。相對的,藉由emplace成員函式來加入的元素,則會是以直接初始化的方式來初始化的,因為它是藉由「建置」元素的方式來加入元素的。(§9.3.1,頁345)【由此可以知道所謂的「直接」,是與「建置」有關的,也就是無中生有。而拷貝則是有所依傍的,就不是「直接」或「獨立」的。「直接」就是沒有依傍的、是原生的,不是副本或複本的。】
Parameters and Return Values 參數與回傳值
5:17:30 5:27:40 5:49:20
在調用函式期間,不具參考型別的參數是藉由拷貝初始化來傳遞其引數的。(§6.2.1,頁209)同樣的,當一個函式的回傳值並非參考型別時,所回傳的值也是藉由拷貝初始化的方式來傳遞給呼叫端儲存呼叫結果的變數。(§6.3.2,頁224;因為函式呼叫一定是用「=」指定運算子(assignment operat)來指定給呼叫端儲存函式運算結果的變數的)
當一個函式有非參考的回傳型別,其回傳值會被用來拷貝初始化位於呼叫位置的呼叫運算子之結果(§6.3.2)。(中文版)
拷貝建構器之所以它的參數必須是個參考,就是因為拷貝建構器它是用來初始化非參考型別的參數的。只要參數是非參考型別的,就必須調用拷貝建構器來將之初始化。那麼,如果拷貝建構器的這個參數不是一個參考型別,在傳遞這個引數時,就必須再調用拷貝建構器將之初始化。如此循環下去,拷貝建構器該做的工作就永無得以進行的一天了。因為呼叫拷貝建構器,就需要用到拷貝建構器來拷貝引數,然而為了要拷貝引數,又必須再次調用拷貝建構器,如此自為因果,循環下去,沒完沒了。所以拷貝建構器的第一個參數就決定不能是個非參考型別的參數。
5:54:20
Constraints on Copy Initialization 拷貝初始化的限制
如果使用的初始器須經由explicit建構器(§7.5.4,頁296)來進行轉換的話,那是到底是要用拷貝初始化、還是直接初始化才好,就有差別了:
vector<int> v1(10); //直接初始化是OK的
vector<int> v2 = 10; //這是錯的,因為帶著一個容器大小(這裡是「10」)作為引數的vector建構器是explicit的建構器【可見要建置就不能用拷貝,要用直接的方式】
void f(vector<int>); //f這個函式的定義,其參數是經由拷貝初始化的
f(10); //這是錯的:因為f的參數型別是vector,而帶了一個引數的vector建構器(可以作隱含轉型的建構器是只帶一個參數的建構器),卻是explicit的,也就是「10」不能隱含轉型為vector。所以這裡用拷貝的方式來將此「10」這個引數傳給vector這個explicit的建構器是錯誤的。因為函式的引數(這裡為「10」),只要不是參考型別,都是用拷貝的方式來傳遞的。
f(vector<int>(10)); //明確調用「即()」只含一個參數的vector建構器:以「10」這個int來建置一個臨時的10個元素大小的vector容器,來作為f函式的引數傳遞是可以的
直接初始化v1是OK的,但類似的以拷貝初始化的方式來初始化v2卻是不行的,這是因為vector帶了一個大小作為參數的建構器是explicit的。和不能以拷貝初始化的方式來初始化v2一樣的理由,我們要傳遞一個引數、或者說從函式回傳值時也不能直接地(隱含地)用到explicit式的建構器。如果要用上explicit式的建構器,我們就必須以明確地方式來調用這個只有一個參數的建構器,如上面所舉例中最後一行一樣。
「vector<int>(10)」的「()」就是調用建構器的呼叫運算子(call operator)
頁499
6:29:40
The Compiler Can Bypass the Copy Constructor 編譯器會繞過拷貝建構器
6:30:00 沒錄到的部分就請看臉書直播第529集約4:30:00前後 https://www.facebook.com/oscarsun72/videos/2627278787383239/
在要做拷貝初始化的工作時,編譯器是可以、但不是必然會跳過拷貝建構器與移動建構器,而直接建置物件的。【即不「鳥」拷貝初始化的指令,而直接用直接初始化來操作。可見移動建構器和拷貝建構器也是同一國的,而建置、與直接初始化的概念,則是另一端的。】意思就是說,編譯器是會將下列這行:
string null_book="9-999-9999-9";//拷貝初始化
改寫成:
string null_book("9-999-99999-9");//這樣編譯器就跳過了拷貝初始器。「()」這個呼叫運算子就是在呼叫string的建構器
然而,即使編譯器會略過拷貝建構器或移動建構器,但是拷貝建構器與移動建構器卻必須在程式執行的那個時間點上是存在的,且是可以存取的(比如說,它不能是private的)。【建構器還有private的嗎?】
練習13.1
什麼是拷貝建構器,它會在何時派上用場?
只要物件在進行拷貝時都會用到拷貝建構器
練習13.2
6:54:59
解釋下列的宣告為何是錯的?
Sales_date::Sales_data(Sales_data rhs);
「Sales_data rhs」一定要是「Sales_data & rhs」;因為函式(建構器)的參數若不是參考,就會調用拷貝建構器來將之拷貝初始化。如此則無限循環。所以建構器參數,決定不能是它自己類別的非參考型別。
練習13.3
6:40:10 6:55:55
當我們拷貝一個StrBlob物件時,會發生什麼事?若拷貝StrBlobPtr的物件呢,又會發生什麼事?
因為沒有自定義的拷貝建構器,所以編譯器會代勞湊合出一個拷貝建構器,這個建構器會將所有的資料成員,依其類別的屬性來進行拷貝初始化。
在拷貝StrBlob物件時,會拷貝智慧指標shared_ptr,使其參考計數增1。
std::shared_ptr<std::vector<std::string>> data;
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/page474_class_StrBlobPtr/prog1/StrBlob.h
而StrBlobPtr則會就以下二個成員的型別,分別進行拷貝:
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr;
練習13.4
6:50:50 6:57:43
假設Point是個類別型別(class type),它帶著一個公用的public拷貝建構器,請在下列程式碼中指出何時會調用這個拷貝建構器:
Point global;//如果這個類別型別有個預設建構器是要拷貝其資料成員,則就會動到這個拷貝建構器
Point foo_bar(Point arg)//引數arg傳遞時也會動到Point的拷貝建構器
{
//「=」就會動到。「Point(global)」也會動到
Point local = arg, *heap = new Point(global);
*heap = local;//也會動到
Point pa[4] = {local, *heap};//也會動到
return *heap;//以值回傳Point時就會動到其拷貝建構器
}
練習13.5
7:18:10
如下面這樣的類別定義,試著針對它寫出一個拷貝建構器來拷貝它所有的資料成員。這個拷貝建構器應該要能動態配置出一個新的string(§12.1.2,頁458)以拷貝ps指標指向的物件,而不是只是拷貝ps而已:
class HasPtr
{
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {}
private:
std::string *ps;
int i;
};
7:37:50
#include<string>
class HasPtr
{
public:
HasPtr(const std::string& s = std::string()) :
ps(new std::string(s)), i(0) {}
HasPtr(const HasPtr& ) :ps(new std::string(*ps)),i(i){}//拷貝建構器
private:
std::string* ps;
int i;
};
int main() {
HasPtr hp;
HasPtr hp2("守真"),hp1(std::string("阿彌陀佛"));
hp = hp1;
hp1 = hp2;
}
頁500
13.1.2. The Copy-Assignment Operator拷貝指定運算子
7:38:30
就像類別能夠決定它的物件是如何被初始化的,它當然也能決定如何指定值給它的物件。
Sales_data trans, accum;
trans = accum; //這裡用到了Sales_data的拷貝指定運算子
如果一個類別沒有定義自己的拷貝指定運算子,編譯器也會像對拷貝建構器那樣地為類別湊合出一個來用。
什麼叫重載(重新定義)指定運算
在瞭解編譯器湊合的指定運算子之前,我們有必要先來瞭解一下什麼叫做運算子的重載,雖然到14章我們才會更深入地瞭解它。
運算子的重載(重新定義)即是在定義一個函式時,於其「operator」這個關鍵字後接著要被重載的運算子符號。因此指定運算子其實就是一個名為「operator=」的函式。一如其他的函式,運算子函式也有它的回傳型別與參數列(parameter list)。
重載運算子函式的參數列代表了這個運算子的運算元。有些運算子,如指定運算子,必須定義成類別的成員函式才行。當運算子是成員函式時,它左邊的運算元是和隱含的8:21:00(即沒在參數出現、寫明的)this參數結合在一起的。(§7.1.2,頁257)而二元的運算子的右邊運算元,如指定運算子的右運算元,則是以明確explicit參數的方式來傳遞的。7;54:50 8:30:00【意指不能隱含轉型,也不能用預設值省略此引數】拷貝指定運算子帶了一個與其類別同型的引數:
class Foo
{
public:
Foo &operator=(const Foo &); //指定運算子
//....
};
為了要維持與在對內建型別作指定運算時一樣的效果(§4.4,頁145),指定運算子通常回傳的會是一個對它左運算元的參考。還要注意的是程式庫通常會要求將要放置在容器內的型別,其定義的指定運算子之回傳值也必須是一個對其左運算元的參考。
養成好習慣
指定運算子回傳的通常都應該要是對它們左運算元的參考。
由編譯器湊合出來的拷貝指定運算子
8:4:10 8:23:19
如果一個類別並沒有定義它自己的拷貝指定運算子,那麼編譯器也會像對拷貝建構器一樣的方式來湊合出一個該類別的拷貝指定運算子。就如同拷貝建構器,有些類別湊合出來的拷貝指定運算子卻不能用來進行指定運算(§13.1.6,頁508)。除了這種情形之外,由編譯器湊合出來的拷貝指定運算子會將右運算元的類別物件,其非靜態成員依其各成員的型別、利用其可用之拷貝指定運算子來進行逐一地指定給左運算元類別物件所對應的成員。而陣列中的成員,則會藉由逐一將其元素指定的方式來做指定運算。【也就是一個個元素物件內的每個成員,逐一進行拷貝指定】這個湊合出來的拷貝指定運算子回傳的就是一個對其左運算元物件的參考。
頁501
舉個具體的例子來說,下列程式碼所演示的就相當於編譯器為尚未定義其拷貝指定運算子的Sales_data所湊合出來的拷貝指定運算子:
//以下的作用,就相當於編譯器為Sales_data所湊合出來的拷貝指定運算子
Sales_data &
Sales_data::opertor = (const Sales_data &rhs) //rhs:應是right-hand Salse_data的意思
{
bookNo = rhs.bookNo; //調用string型別的拷貝指定運算子(string::operator=)
units_sold = rhs.units_sold; //使用內建型別int的指定運算(子)
revenue = rhs.revenue; //使用內建型別double的指定運算(子)
return *this;
}
練習13.6
8:35:30
什麼是拷貝指定運算子?何時會用到它?而編譯器湊合出來的拷貝指定運算子又會做些什麼事情?什麼時機下會由編譯器湊合這樣的運算子出來?
8:37:40
練習13.7
當對StrBlob作指定運算時會發生什麼事?若是對StrBlobPtr做指定運算呢?
複製(拷貝)StrBlob物件後會遞增其data智慧指標shared_ptr的參考計數
程式碼詳:https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise13_07/prog1
9:1:10
練習13.8
寫出在§13.1.1練習13.5(頁499)中HasPtr類別的指定運算子的定義。就猶如對拷貝建構器的習作一樣,您所定義出來的指定運算子也應該是要去拷貝ps指向的物件,而不是ps本身。
9:12:10
程式碼詳:https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise13_08/prog1/prog1.cpp
13.1.3. The Destructor 解構器
9:26:10
解構器的操作與建構器適得其反。建構器會將類別物件的非靜態的資料成員初始化,並可能也進行其他必要的運算。解構器則是專注在釋放一個物件所佔用的系統資源,並摧毀該物件內所有的非靜態的資料成員。
解構器是一個類別的成員函式,它的名稱就是其類別的名字,並在其前再冠以「~」這樣的波浪號。它不會有參數,也不會回傳值。
class Foo
{
public:
~Foo(); //這就是解構器
//...
};
解構器既然沒有參數,它當然就不能被重載(overload)。一個類別永遠只會有、也只能有一個解構器。
解構器都做些什麼事呢?
9:30:40
如同建構器有其建構器初始器串列部分及它自己的函式本體部分(§7.5.1,頁288),解構器也有著一個函式本體的部分及進行解構工作的部分。在建構器中,資料成員的初始化是在其函式本體執行前就完成的,且其各個資料成員的初始化順序是依其在類別中出現的次序來挨次進行的。而解構器中,它的函式本體部分則是優先執行,然後才對其資料成員進行一一地銷毀;而這些資料成員被摧毀的順序,則適與其被初始化的順序相反;也就是說,最先被初始化的資料成員,會最後才被解構摧毀。頁502
9:39:20 10:27:55
解構器的函式本體會執行任何類別的設計者想要此類別物件做完它最後該做的事情之後,要做的事。
「一個解構器的函式主體會進行類別設計者希望在物件用完之後執行的任何作業。」(中文版)
一般來說,解構器會做的就是將該物件在生命週期中配置出來資源,釋放乾淨。
解構器並沒有類似於建構器那樣的初始器串列,來規範它類別物件的資料成員要如何地被摧毀。解構資料成員的部分是隱含、默認地被執行的。資料成員在被解構時會發生什麼事,是由其各自型別來決定的。如果資料成員是類別型別的話,它就會被它自己所屬類別的解構器給銷毀。而內建型別的資料成員並沒有它適用的解構器,所以對於內建型別的資料成員而言,當它被解構時並不會執行任何額外的動作。
注意:內建型別指標的資料成員它在被逕行(隱含)解構時並不會連帶摧毀(delete,清除、刪除)它所指向的物件。
不像一般的指標,智慧指標(§12.1.1,頁452)其實是類別型別的物件,它也有它自己適用的解構器。因此,智慧指標並不像一般普通指標那樣,如果資料成員是智慧指標型別的話,它就會自動地在適當時機下去清除它所指向的物件(不管這物件是不是配置在動態記憶體區)。
As a result, unlike ordinary pointers, members that are smart pointers are automatically destroyed during the destruction phase.
When a Destructor Is Called
何時會用到解構器? 解構器執行的時機
10:2:22 10:35:15
類別物件的解構器會在它被摧毀時自動被調用:
區域變數離開了它建置的範疇(生命週期結束)
當物件被摧毀時,它的資料成員也會被一一地摧毀。(類似元素物件(如資料成員)在容器(如類別物件)中的概念)
「一個物件的成員會在它們是其一部分的物件被摧毀時摧毀。」(中文版)
Members of an object are destroyed when the object of which they are a part is
destroyed. ⑦先抓動詞 ⑧找對主詞
不管是在程式庫型別的容器還是陣列內的元素,在容器或陣列被摧毀時也會隨之被摧毀,此時就會調用其型別相應的解構器。
當delete運算子套用到一個指標(§12.1.2,頁460),該指標所指向的動態配置的物件(dynamically allocated object)被摧毀時,該物件型別的解構器就會被調用。
在一個表達式執行結束時,在執行它的期間所建置出來的暫存物件(temporary objects)被摧毀的時候,此暫存物件其型別的解構器就會被調用。
因為解構器是自動(隱含、逕自)被執行的,因此我們在設計程式時只要操心如何、又何時去配置適當的資源,而通常並不需要去管我們所配置出來的資源,何時、又如何會被釋放。
比如說在下列程式碼的片段中,我們定義了4個Sales_data的物件:
{ //一個新的範疇
// p 和p2指向的是動態配置的物件
Sales_data *p = new Sales_data; //p是一個內建型別的普通指標
auto p2 = make_shared<Sales_data>();//p2則是一個shared_ptr的智慧指標
Sales_data item(*p); //拷貝建構器會將p解參考後的物件拷貝給item變數
vector<Sales_data> vec; //區域性的物件(區域變數)
vec.push_back(*p2); //將p2所指向的物件,以拷貝的方式加入到vec容器中
delete p; //在p所指向的物件上調用了該物件型別的解構器
} //離開區域範疇,身為區域變數的item、p2、vec就會被摧毀,它們各自對應到的解構器就會被調用
//在摧毀p2這個shard_ptr的智慧指標時,它的參考計數(use count)就會被遞減1,如果這計數歸零,那麼它所指向的物件就會被清除
//摧毀容器vec的同時,也會將其內元素一一地摧毀(按照元素之型別,調用其類別之解構器,或內建型別的話,就直接清除)
10:48:20 11:5:48
頁503
上列的每個物件都含有一個string的資料成員,這資料成員是由動態配置的,以儲存bookNo這個Sales_data的資料成員所含有的字元。然而在這段程式碼中,由我們直接來管控的記憶體,僅僅也只有那個我們用new直接配置出來的那個。我們這段的程式碼也僅只釋放了那個指標p所指向的、由我們用new直接配置出來的Sales_data物件。
其他的Sales_data物件則會在離開這個區域範疇時被自動摧毀。當這個範疇結束時,vec、p2、和item都會超出了它們自己的生命週期,也就是vector、shared_ptr和Sales_data這3個類別的解構器將會分別套用在這3個對應的物件上。vector的解構器會將我們加入到它裡面的元素一一地摧毀。而shared_ptr的則是會遞減參考到p2所指向之物件的智慧指標的參考計數。在此例中,這個計數將會歸零,也就是shared_ptr的解構器就會刪除由p2配置出來的那個Sales_data物件。
10:59:05 11:9:35
在上述所有的情況下,Sales_data類別的解構器都是逕自(implicitly)被調用的,以摧毀其內的bookNo這個資料成員。要摧毀這個資料成員則會調用string類別的解構器,這個解構器會將用來給bookNo存放ISBN值的資源給釋放掉。
注意:當指向一個物件的參考或指標超出了其區域範疇,它們所指向之物件的解構器並不會被調用。
「對一個物件的參考或指標超出範疇時,解構器不會執行。」(中文版)
The destructor is not run when a reference or a pointer to an object goes out of scope.
The Synthesized Destructor 由編譯器湊合出來的解構器
11:11:11 11:30:33
編譯器會為任何沒有自行定義解構器的類別拼湊出(湊合出)一個合成的解構器。就如對拷貝建構器和拷貝指定運算子那樣,編譯器為某些類別所合成出來的解構器卻不能將其型別的物件給解構掉。(§13.1.6,頁508)除此之外,一個由編譯器湊合、合成出來的解構器,它的函式本體會是空的。
舉例來說,編譯器會為Sales_data類別定義(拼湊、湊合、合成)出如下的解構器來:
class Sales_data
{
public:
//除了自行逐一摧毀其中的資料成員,這樣由編譯器合成出來的解構器實際上並不會做其他的事
~Sales_data() {}
//和前面所舉一樣,這裡是其他剩下的類別成員
};
在空的函式本體執行過後,Sales_data物件的資料成員就會自動地依序被滌除乾淨。而string類別的解構器則會被調用來清除bookNo資料成員所佔用的記憶體資源。
很重要的是要知道解構器的函式本體部分並不會直接參與其資料成員的解構工作。對其資料成員的解構(摧毀)工作是在解構器的函式本體執行後,才會逕自(隱含地被)進行。解構器的函式本體部分的執行,和對資料成員做逐一的清除工作,都是摧毀一個物件時會發生的事。
11:26:24 11:34:20
13.1.4. The Rule of Three/Five
一如我們所見,有3個有關控管類別物件進行拷貝的基本操作:
拷貝建構器、
拷貝指定運算子、以及
解構器。
甚至我們還會在§13.6(頁531)時看到,在新標準下,類別還能定義它自己的移動建構器和移動指定運算子。
頁504
11:35:10
練習13.9
什麼是解構器?而由編譯器合成出來的解構器,又會進行怎樣的工作?什麼時機下編譯器為代某個類別來合成出一個湊合著用的解構器呢?
11:36:51
練習13.10
StrBlob物件被摧毀時會發生什麼事?如果是摧毀StrBlobPtr物件時呢?
練習13.11
11:45:22
將之前練習題中做過的HasPtr類別加入一個解構器
程式碼見:https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise13_11/prog1/prog1.cpp
練習13.12
11:52:40
沒錄到的部分請見臉書直播532集 https://www.facebook.com/100003034306665/videos/2638520136259104
在下列程式碼片段中會進行多少次的解構器調用?
bool fcn(const Sales_data *trans, Sales_data accum)
{
Sales_data item1(*trans), item2(accum);
return item1.isbn() != item2.isbn();
}
應是item1、item2、accum三次調用解構器。trans出範疇後只是銷毀自身(指標)其所指之Sales_data物件並不會被銷毀,即不會調用其解構器
練習13.13
11:56:20
留言