C++自修入門實境秀、C++ Primer 5版研讀秀 84/ ~12.1. Dynamic Memory and Smart Pointers...
改中文版作文:第84集起始
習題章節12.1.3 11:05
練習12.10 :
下列呼叫前面§ 12.1.3中定義的process函式的寫法是否正確。如果有誤,該如何更正這些寫法?
// ptr會在process被呼叫時創建並初始化
void process(shared_ptr<int> ptr)
{
//使用ptr
} // ptr超出範疇,並被摧毁了
shared_ptr<int> p(new int (42));
process(shared_ptr<int>(p));
可以,但不需要。「process(shared_ptr<int>(p));」改成「process(p);」即可,因為p在呼叫process時已是一個shared_ptr了
測試程式碼見: https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise12_10
練習12.11 :31:35 1:51:30
如果像這樣呼叫process,會發生什麼事?
process(shared_ptr<int>(p.get()));
process執行完成後,p會變成懸置指標!
練習12.12 :
使用下列p和sp的宣告定義(declarations)來對process呼叫。如果呼叫式是合法的,請說明它會做些什麼事。如果不是,請解釋原因:
auto p = new int ();
auto sp = make_shared<int>();
(a) process(sp);
(b) process(new int ());
(c) process(p);
(d) process(shared_ptr<int>(p));
練習12.13 :
2:1:20
如果我們執行下列程式碼,會發生什麼事?
auto sp = make_shared<int>();
auto p = sp.get();
delete p;
auto sp = make_shared<int>(1);
auto p = sp.get();
//sp.reset(p);//這也會出錯!大概是不能用sp本身get出來的普通指標來作reset引數;
//或reset引數的指標必不能與sp指向同一個記憶體位址?非也!
//因「如果p是指向其物件的唯一一個shared_ptr,reset會摧毀其物件。」在此例reset會摧毀sp
//所指之物件,故在測試中sp與p在reset後同時失效--成了懸置指標,故後續執行會出錯
//delete p;//會使用sp成了懸置指標,執行時也會出錯。此行當改用下式來釋放p
sp = nullptr;//此時p成了懸置指標
p = nullptr;//此時p成了空指標
12.1.4智慧指標和例外情形
2:17:25
在§5.6.2(頁196)中,我們留意到一個應用程式在碰到例外情形而使用例外處理程序做善後時,必須確保應當釋放的資源有正確地被釋放。要確保資源在這種狀況下也能得到妥善地釋放,使用智慧指標就是一個簡易的方法。
當我們使用智慧指標來管控記憶體,智慧指標類別能確保記憶體在沒用時會被釋放,即使不是在正常情況結束程式(even if the block is exited prematurely),也會如此:
void f()
{
shared_ptr<int>sp(new int(42)); //配置一個新的shared_ptr所指的物件「42」int
//這裡的程式碼會丟出在f內沒有被捕捉到的例外情形
} // 區域變數shared_ptr sp會在函式終止(ends)時自動被摧毀,並釋放(freed)其所控管的記憶體資源
退出一個函式時——不管是經過正常程序執行終了,或因為例外情形而意外終止——其函式所有的區域物件沒有例外、都會被摧毀。在此例中, sp是一個shared_ptr,所以在摧毀sp時會檢查其相關的參考計數。這個sp是它所控管記憶體上的唯一智慧指標,所以在摧毀sp的過程中這個記憶體會得到釋放。
然而,經由直接管理的動態記憶體卻並不會在例外發生時自行得到釋放。如果我們使用內建的普通指標來直接管理記憶體,當例外情形發生在一個new之後、在對應的delete前,那麼那個記憶體就得不到釋放:
擷取Visual Studio 2019 的程式碼配色 41:00 1:34:00
設定色彩佈景主題和字型
臉書直播實境秀 https://www.facebook.com/oscarsun72/videos/2462591403851979
Word VBA做userform
58:57 Word VBA Enum(列舉)的撰寫
51:00 1:20:30 1:24:00 測試成功!
程式碼與本檔存在一塊,按Alt+F11便可查看了。
改中文版作文:
2:58:03
void f()
{
int *ip = new int(42); //動態配置一個新的物件
//這裡的程式碼會丟出一個沒有在f內被捕捉的例外
delete ip; //在退出前釋放記憶體
}
如果例外在new和delete之間發生,而且沒有在f內被捕捉到,那麼這個記憶體就永遠不會被釋放。因為在函式f外並沒有指標指向這個記憶體位址,因此,這個記憶體就無法被釋放。
智慧指標和一種特殊的類別——窘類別(囧類別)(不智類別Dumb Classes)——會配置資源卻沒有好好定義解構器(destructor)的類別
5:24:00
3:29:30 原來Smart和Dumb是相對的
就是智與不智,聰明與呆笨也
智慧vs不才
3:6:45
許多C++類別,包括所有的程式庫類別,都有定義解構器(destructors,§12.1.1)來負責清理各種物件用過的資源。然而,並不是所有的類別都如此盡職。特別是,那些設計來讓C和 C++可以兼容的類別,一般都會要求使用它們的要記得明確釋放用過的資源。
那些會配置資源,卻沒有定義解構器來釋放那些配置資源的類別,就很有可能會遇到我們使用動態記憶體時常犯的錯誤——很容易就忘記了釋放它配置的資源,在用過這些資源之後。同樣地,如果例外情形發生在該資源配置後、及其被釋放之前,那麼程式就有耗漏資源的風險。
我們通常可以使用管理動態記憶體的那套模式方法(那個套路)來管理沒有定義完善解構器的類別。舉例來說,想像我們正在使用C和C++都能使用的一個網路程式庫。使用這個程式庫的程式可能會包含像這樣的程式碼:
connection運算的定義
struct destination; //代表我們要連線的對象
struct connection; //包含了所有連線的相關資訊
connection connect (destination*) ; //啟用連線
void disconnect (connection) ; // 切斷關閉某連線
void f (destination &d /* 其他的參數 */)
{
//取得一個連線;必須記得在完成後關閉它
connection c = connect(&d);
//使用這個連線
//如果我們忘記在退出前呼叫disconnect,就沒有辦法關閉c 了
}
如果connection有一個解構器,那個解構器會在f執行完畢時自動關閉連線。然而, connection並沒有解構器。這和我們前面曾有一個利用shared_ptr來避免記憶體耗漏的程式其所面臨的問題幾乎相同。故其實我們也能利用shared_ptr來控管connection物件c以確保connection c的連線能被妥當地(properly)關閉。
使用我們自訂的清除資源(Deletion)程式碼
預設情況下,shared_ptr都會認為它們自己是指向動態記憶體的。所以,當一個shared_ptr被摧毀時,它就會在它所持有的指標上逕行delete運算。所以我們就可以利用它們這樣的特性,來將如connection這般不智的類別(dumb classes)物件「偽裝」成在動態記憶體區配置的物件,讓shared_ptr來管控這樣的類別物件。
改中文版作文:
4:10:44
若要像這樣利用shared_ptr來管控connection類別物件,我們就必須先自行定義(自訂)一個函式來取代直接運用 delete運算子來進行清除資源的工作。必須要以儲存在shared_ptr內的指標來呼叫這個自訂刪除器(deleter)函式,才能對這個shared_ptr所指向的物件進行刪除。 因此在這裡,我們的刪除器就必須是一個有型別為connection*(對connection指標)參數的函式:
void end_connection(connection *p) { disconnect(*p); }
當我們創建一個shared_ptr,我們可以傳入一個選擇性的引數指向一個刪除器函式(§6.7,頁247,Table 12.3. Other Ways to Define and Change shared_ptrs:shared_ptr<T>p(q,d)、shared_ptr<T>p(p2,d)):
void f (destination &d /* 其他的參數 */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
//使用這個連線
//當f正常執行結束,或即使是因為發生了例外狀況而中止,這個連線仍會適當地(properly)被關閉
}
當智慧指標p被摧毀時,因為建構它時引入了end_connection這個引數,它就不會在它所儲存的指標上以delete進行清除的動作。而是會用那個指標去呼叫 end_connection這刪除器(deleter)以進行清理資源的工作。在刪除器end_connection執行時,則會呼叫disconnect這個成員函式,這樣就可以確保連線有被正確地關閉。如果f函式能夠正常結束,那麼區域變數智慧指標p就會在函式進行回傳(return)動作時被摧毀。甚至,即使在f函式執行的過程中發生了例外,這個p也一樣會被摧毀,而其管控的連線也仍可得以關閉,因為在p創建時,已同時帶入end_connection來作為清除資源之用,其效用,就等同於delete。
注意:使用智慧指標須注意的事項
只有正確使用智慧指標,智慧指標才能讓控管動態配置物件(memory) 及動態記憶體區(dynamic memory)的管控變得更為可靠(safety)與便利。若要正確使用智慧指標,我們就必須遵守以下原則:
•別用同一個內建型別的普通指標來初始化(或reset)多個智慧指標。
•別在get()回傳的普通指標上進行delete運算。
•別使用get()回傳的普通指標來初始化或reset另一個智慧指標。如此做也就類似第1條的一對多了—一個是普通指標卻去初始化或指定給多個指標。
•如果非得要用到 get()所回傳的一個普通指標,那麼就要記得這個普通指標會在和它同一指向的智慧指標都消失後,變得無效,淪為懸置指標。因為智慧指標的參考計數歸0時,都會直接刪除它所指向的物件。
•雖然智慧指標預設是設計用來給動態配置記憶體用的,但是,智慧指標也可以用來管理一般記憶體區上的物件。因此,如果使用了智慧指標來管理那些不是new在動態記憶體區所配置出來的資源,那麼我們就必須使用自訂刪除器來取代delete運算子。(§12.1.4,頁468、§12.1.5,頁471)。因為delete是搭配new來使用的,現在不用new出來的資源,就不能再用delete來進行該資源的釋放工作。
習題章節12.1.4
練習12.14 :
5:47:10 5:53:40
寫出一個你自己版本的函式,用shared_ptr來管理connection。
關鍵在於如何清除窘類別(dumb classes)connection無法清除的資源
struct destination {}; // represents what we are connecting to
struct connection {}; // information needed to use the connection
connection connect(destination*) { connection c; return c; } // open the connection
void disconnect(connection) { ; } // close the given connection
//void end_connection(connection* p) { disconnect(*p); }//課本用普通指標
void end_connection(shared_ptr<connection>& p) { disconnect(*p.get()); }//我們用智慧指標
void f(destination& d /* other parameters */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c);
end_connection(p);
}
int main() {
destination d;
f(d);
}
練習12.15 :
6:29:00
改寫第一個練習,這次使用lambda ( § 10.3.2,頁388)來取代end_connection函式。
struct destination {};
struct connection {};
connection connect(destination*) { connection c; return c; }
void disconnect(connection) { ; }
void end_connection(connection* p) { disconnect(*p); }
void f(destination& d /* other parameters */)
{
connection c = connect(&d);
//shared_ptr<connection> p(&c, end_connection);
//改寫第一個練習,這次使用lambda ( § 10.3.2,頁388)來取代end_connection函式。
shared_ptr<connection> p(&c, [](connection* c){disconnect(*c); });
}
int main() {
destination d;
f(d);
}
VisualStudio簡介及安裝
第84集 5:14:00
5:24:00「工作負載」也是文言文,不是專業的誰看得懂什麼意思?
5:28:30
改中文版作文:
7:1:10
12.1.5 unique_ptr
一個unique_ptr「獨佔了(owns)」它所指向的物件。與shared_ptr不同,同一個時間下只能有唯一一個 unique_ptr可以指向某個物件。一個unique_ptr所指的物件,被它獨佔、被它管控,故在那個unique_ptr 摧毀時也只能跟著陪葬。表12.4列出了專屬於 unique_ptr的運算。與shared_ptr共通的運算則列於表12.1(頁452)中。
與shared_ptr不同,並不存在類似於make_shared的程式庫函式,來回傳一個unique_ptr。
因此,當我們在定義一個unique_ptr的同時,通常就會把它和new所回傳的一個普通指標繫結(bind)起來。就如同shared_ptr一樣,要初始化一個unique_ptr時,必須使用直接形式的初始化才行:
unique_ptr<double> p1; // 一個指向double 型別的 unique_ptr 叫做p1,未經初始化
unique_ptr<int> p2(new int (42)) ; // p2 是指向值為 42 的 int—這就是在宣告、定義的unique_ptr的同時,將它與new出來的普通指標繫結(bind)在一塊;也就是p2指向了new回傳的普通指標所指向的動態配置物件,二者是該物件同時間指向它的兩個指標
因為一個unique_ptr「獨占了」它所指的物件,所以unique_ptr當然不支援一般的拷貝或指定(指定就是拷貝、複製一份副本了),不與它人——其他指標「share」它所指的物件:
unique_ptr<string> p1(new string("Stegosaurus")); unique_ptr<string> p2(p1); // 錯誤:unigue_ptr 沒有拷貝運算
unique_ptr<string> p3;
p3 = p2; // 錯誤:unique_ptr 也沒有指定運算
Table 12.4. unique_ptr Operations (See Also Table 12.1 (p. 452))
雖然無法拷貝或指定一個unique_ptr(除了可指定為nullptr),我們卻可以將一個非 const的unique_ptr對其物件的獨佔權(ownership)讓渡給另一個unique_ptr,此時就須用上release或reset 這兩個「方法=成員函式」:
//將獨佔權從p1 (它指向的是「Stegosaurus」這個string)讓渡給p2
unique_ptr<string> p2(p1.release()) ; // release 後使得 p1 的值成了null空指標
unique_ptr<string> p3(new string("Trex"));
//將獨佔權從p3讓渡給p2
p2.reset(p3.release()); // reset 會清除p2 曾指向的記憶體
release成員函式回傳目前儲存在unique_ptr中的普通指標,並使那個unique_ptr其值成為null——也就是成了空指標nullptr。這裡,p2會以曾在p1中的指標值來初始化,而p1會變為空指標。
改中文版作文:
7:45:50
reset成員函式則接受一個選擇性的指標,並重新將unique_ptr指向所給指標指向的物件。如果這個unique_ ptr本來不是null,那麼它原指的物件就會被刪除。在這裡對p2進行reset的呼叫,會釋放那個以"Stegosaurus"初始化的string所佔用之記憶體,將p3的指標讓渡給p2,並使p3 的值變為null。
release的呼叫會中斷unique_ptr和它所管控物件之間的關係。通常release回傳的指標會被用來初始化或指定給另一個智慧指標。在這種情況下,管理這個記憶體的責任就只是從一個智慧指標轉移到另一個。然而,若我們沒將 release回傳的指標儲存在一個智慧指標中,交它來管控,那麼我們的應用程式就必須負責那個資源的釋放工作:
p2.release() ; //錯了:p2因此並不會釋放它所佔用的記憶體,而我們反而會因此失去對那個記憶體的控制(即失去指向該記憶體位址的指標)
auto p = p2.release ();//ok,但我們必須記得在p所指向的資源不再用到時,要對p進行 delete運算,以釋放其記憶體資源
傳遞unique_ptr與回傳unique_ptr
「無法對unique_ptr進行拷貝」這個規則有一個例外:那就是我們可以拷貝或指定即將被摧毀的 unique_ptr。因為它是「unique」(獨佔性的),只要不違反unique的原則下,當然都是可以的{懂得道理好修行}。既然是要被摧毀了,當然殘存下來的,依然就只有一個——也就是「unique」!最常見到的情形就是從一個函式回傳一個unique_ptr:
unique_ptr<int> clone (int p){
// ok :從int*明確創建一個 unique_ptr<int>
return unique_ptr<int>(new int(p));
}
又或者,我們也可以回傳一個區域物件的拷貝:
unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int(p));
//…
return ret;
}
編譯器當然知道在這兩種情況下,回傳的物件就要消逝。因此,它就會進行一種特殊的「拷貝」,使得這個unique_ptr不會與「unique」原則衝突;這會在§ 13.6.2(頁534)中討論。
回溯相容性:auto_ptr
早期版本的程式庫還包括了一個叫做auto_ptr的類別,它具有unique_ptr的部分屬性,但非全部——尤其是,無法將一個auto_ptr儲存在容器中,也不能將它從一個函式回傳。所以較諸unique_ptr它的功能甚是有限的。
雖然auto_ptr仍是標準程式庫的一員,我們要寫程式時還是該優先選用unique_ptr,而不要再沿用auto_ptr。
將刪除器傳入unique_ptr中
就像shared_ptr,在預設情況下, unique_ptr也是用delete運算子來刪除它所指向的物件。和shared_ptr 一樣,我們也可以在unique_ptr中覆寫那個預設的刪除器(default deleter,§12.1.4,頁468、469)。然而,unique_ptr管理它刪除器的方式卻與shared_ptr有所不同,原因我們會在§16.1.6(頁676)中描述的。
改中文版作文:
8:32:40
覆寫一個unique_ptr中的刪除器會影響到unique_ptr的型別,以及我們如何建構(或reset)該型別物件的方式。類似於覆寫關聯式容器的比較運算(§11.2.2,頁425),我們必須在角括號 (angle brackets)中提供那個刪除器的型別,以及unique_ptr指向的型別。我們會在創建或 reset這種型別的unique_ptr物件時明確指出該可呼叫物件的型別是什麼:
// p這個unique_ptr物件指向型別為okjT的一個物件,並使用型別為delT的一個可呼叫物件來清除那個okjT物件
//p會呼叫型別為delT而名為fcn的物件來作delete的工作
unique_ptr<objT, delT> p(new objT, fcn);
8:45:10為了更具體地掌握這樣的unique_ptr是怎麼定義與運作的,我們可以用unique_ptr取代shared_ ptr來改寫我們先前的那個連線程式:
void f(destination &d /*其他所需的參數*/)
{
connection c = connect(&d) ; // 開啟連線
//當p被摧毁,連線也會被關閉,因為p是unique_ptr型的智慧指標
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection); //當不用預設的delete來清除動態記憶體時,就得用這樣的語法來定義一個unique_ptr,才能帶入(導入、傳入)一個delete的取代物
//……這裡是使用連線的程式碼……
//當函式f結束後——即使僅是因為發生例外狀況而被迫中止,連線都可以得到正確地關閉,其所佔用的資源也因此得到正確地釋放
}
在這裡,可以看到我們是使用decltype(§2.5.3,頁70、250)來指定函式指標型別。因為decltype(end_connection)回傳的只會是一個精準的函式型別,而不是對函式的指標(function pointer),所以我們必須記得在其後再加上一個「*」來表示我們要用的是對該函式型別的一個指標(§6.7,頁250)。
習題章節12.1.5
8:59:10
練習12.16 :
當我們試著對一個unique_ptr物件進行拷貝或指定操作時(除了將之指定為空指標nullptr),編譯器並不會給出很明確的錯誤資訊。試著寫一個包含了這樣錯誤的程式,看看你的編譯器會出現怎麼樣的診斷資訊。
練習12.17 :
9:5:20
下列哪些程式碼對unique_ptr的宣告是正確的,還是它會導致錯誤?請解釋每一項的問題所在。
int ix = 1024, *pi = &ix, *pi2 = new int(2048);
typedef unique_ptr<int> IntP;
(a) IntP p0(ix) ; //解答參見前練習12.17,餘同。
(b) IntP p1(pi); //(b)竟然連普通取址運算子回傳的指標也可以。只有在編撰時才行;若執行,仍會出錯!//因為智慧指標(不管是shared_ptr或unique_ptr)預設是用delete來清除記憶體資源,而delete是與new搭配的,故若沒有指定刪除器(deleter)來取代delete運算,那麼就一定要時new出來的動態配置的物件才能給智慧指標初始化(作為它的初始器);難怪執行時都會在delete處丟出例外情形!!
(c) IntP p2(pi2) ;//要用new回傳的pointer;因為預設是用delete來清除記憶體的,而delete與new須搭配用
(d) IntP p3(&ix); //和(b)一樣的
(e) IntP p4(new int (2048)); (e)和(c)一樣
(f) IntP p5(p2.get());//(f)這個錯應該是在於p5和p2各有其自己的參考計數(因其彼此不是互相拷貝、指定出來的,而是各自定義出來的物件),當其一個摧毀時,會使另一個成了懸置指標。因此,在p5銷毀前,須將p2對「new int(2048)」的獨佔權釋出、讓渡給p5,使它真正成了unique(獨一無二、獨佔的)才行;即補上以下這行表達式:p2.release();//釋出獨佔權,讓渡給p5;否則二者就會「爭寵」「搶奪」對「new int(2048)」這個動態配置物件的所有權。
練習12.18 :
9:36:50
為什麼shared_ptr沒有release成員呢?
就如同「unique」不能違反其「獨佔」的原則,既然叫「shared」還有什麼好「release」的呢?都已經「shared」了,還有什麼好「release」的呢?根本就是彼此矛盾、衝突、不相容的屬性了。
改中文版作文:
9:40:45
12.1.6 weak_ptr
weak_ptr (表12.5)是一種智慧指標,但它對於它所指向的物件生命並沒有佔有慾(它對它指向的物件生命週期並沒有興趣)。可以說,unique_ptr是佔有慾最強的,而shared_ptr次之(它懂得分享——既以為人己愈有,既以與人己愈多); weak_ptr則最不管事(可以說與窘類別很像)。因此,weak_ptr所指物件的生死是交由某個shared_ptr代為控管的。也因此,將一個weak_ptr和shared_ptr繫結並不會改變那個shared_ptr的參考計數,也就是說,一旦指向該物件的最後一個 shared_ptr消失,不管還有多少weak_ptr指向著它,那個物件本身就會被shared_ptr調用delete或刪除器刪除。也就是weak_ptr這樣的特性,它才會叫做「weak」_ptr,這透露了它在共用物件上是「不強勢(弱性weakly)」的。
weak_ptr既名為智慧指標,因此,當我們創建一個weak_ptr的同時,我們就會用一個shared_ptr作為初始器來將它初始化,以便它所指向的物件能得到有效地控管:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp雖共享了shared_ptr p所指向的物件,但並不會干涉p的參考計數及該物件的生命長度;p中的使用計數(即參考計數(reference count),即成員函式use_count()所回傳的值)不變( wp weakly shares with p; use count in p is unchanged)
這裡wp和p都指向同樣的物件。因為這個共用非是強勢的(軟性、柔性、弱性)的,所以在創建wp的同時並不會動到p的計數參考;而 wp所指的物件也有可能在它還指向它們時就被p刪除。
正因為如此,雖然還有weak_ptr存在,但其所指向的物件可能已不復存在(也就是weak_ptr都成了呆指標—懸置指標了),因此一般的情形下不宜冒然便用一個weak_ptr來存取其所指之物件。若欲存取該物件,就須先呼叫lock成員函式來確定該物件是否還存在。lock函式就會查看weak_ptr所指的物件是否依然存在。若是,lock就會回傳一個shared_ptr來指向那個存在的物件。這個回傳的shared_ptr就跟任何其他的shared_ptr 一樣,都能保證只要shared_ptr的參考計數非0,它所指向的底層物件就沒有被刪之虞,會一直存在到所有的shared_ptr都消失為止。舉例來說:
if(shared_ptr<int> np = wp.lock()){// 如果 np 不是 null 就為 true
//在if內,np會與wp共用其物件(英文版wp誤作p)
}
只會在lock回傳值不是null時,才會進入if的主體。在if內,就能夠安全地使用np 來存取np與wp所指向的那個物件。
Table 12.5. weak_ptrs
指標經過檢查的指標類別
為了示範weak_ptr是幹什麼用的,我們可以為我們的曾定義的StrBlob類別定義一個伴隨的指標類別。我們將這個指標類別,命名為StrBlobPtr,它將會儲存一個weak_ptr來指向它從之初始化的StrBlob的data成員。
留言