C++自修入門實境秀、C++ Primer 5版研讀秀 100-1/ ~12.3.1.~練習12.27-中文版重譯、複習動態記憶體與智慧指標~頁484



忘了錄的部分就看臉書直播513集。

第100集開始

Other shared_ptr Operations

其他的shared_ptr運算

shared_ptr的其他運算(其實也只介紹了reset、unique兩個)

shared_ptr類別還提供了幾個其他的運算,都已列於表12.2和表12.3。我們可以使用reset成員函式(即「重設、重新設定」,重設它所指的對象object)來將一個普通指標指定給shared_ptr,使這個shared_ptr指向新的動態物件:

p = new int(1024) ;//錯!無法將一個普通指標直接指定給一個shared_ptr p

p.reset(new int(1024));//ok:透過shared_ptr類別的reset成員函式,可以將智慧指標p改指向一個普通指標所指的物件

就跟在對shared_ptr做指定(assignment)運算一樣,reset也會更新shared_ptr關聯的參考計數,且在此參考計數歸0時,也會刪除shared_ptr(這裡是「p」)所指的物件。reset成員函式時常會與unique成員函式一起使用,以便妥善管制當要變更shared_ptr指向的物件時所可能造成的影響。通常在變更shared_ptr指向的底層物件前,都會先行確認此shared_ptr是否是對該物件唯一的指標,是的話,才逕行變更。如果這個shared_ptr並不是唯一的指標,而是與其他智慧指標共用此物件資源,那通常就會在更動這個底層物件前先拷貝一份來應付:

if (!p.unique())//p並不是指向物件的唯一智慧指標,

p.reset(new string(*p)); //因此拷貝一個原物件的副本給它

*p += newVal; //智慧指標p現在是唯一指向這個副本物件的指標,變更這個物件就不會出什麼問題——也就是,不會去影響其它共用原物件的指標

頁467

練習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));

(shared_ptr<int>(p))是拷貝,所以智慧指標p的參考計數會+1,成了2,待shared_ptr<int>(p)生命週期結束,p的參考計數-1,仍還有1。所指向的物件並不會被摧毀。因為shared_ptr<int>(p)是由p拷貝而來的。

下列呼叫前面§ 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

11:57

若像下面這樣來呼叫process,又會發生什麼事?

process(shared_ptr<int>(p.get()));

而這個則是犯了「…而且別使用get來初始化或指定另一個智慧指標」

// undefined: two independent shared_ptrs point to the same memory

//未定義:兩個獨立的shared_ptr指向相同的記憶體



這個是獨立創建的一個shared_ptr,因為get回傳的是普通指標,這裡作為shared_ptr建構器的引數傳入,來構建一個新的、獨立的shared_ptr,不是由p來拷貝的,當它死掉時就會逕行刪除它指向的物件,也就是p指向的物件,導致p成了懸置指標。

如果像這樣呼叫process,會發生什麼事?

process(shared_ptr<int>(p.get()));

process執行完成後,p會變成懸置指標!

練習12.12

使用下列p和sp的定義來解釋對process的呼叫,哪些是合法的,哪些是不合法的。合法的,請說明它會做些什麼事;若不合法,請解釋錯在哪裡:

其實p和sp在這裡已是定義,不是宣告而已了declaration 可見這裡定義與宣告是不分的

sp的s就是smart,或shared指智慧指標

p就是內建指標(built-in pointer)或普通指標(plain pointer、ordinary pointer)

auto p = new int();

auto sp = make_shared<int>();

(a) process(sp);//legal,sp參考計數(reference count)會加1,proecss結束後減1。然而如練習12.10,若process內的區域智慧指標結束,要注意防止sp成為懸置指標(dangling pointer) 6:43:00 sp並非區域的,所以不會隨出process範疇而被摧毀。且process內的區域智慧指標雖在process結束後摧毀,但與sp指向同一個記憶體位置,該位置物件仍有sp指向它,故雖然proecss區域的智慧指標摧毀了,也不會釋放該物件之記憶體。

(b) process(new int());//illegal,因為型別不合,不具隱含轉型。new回傳的是一個普通指標、內建指標,而不是智慧指標shared_ptr

(c) process(p); //illegal,同前

(d) process(shared_ptr<int>(p)); //和(a)類似。這裡是將p所指向的物件操縱權交給了process內區域智慧指標,p,就合該無法再對其物件作操作了,p當設為空指標,以免淪為懸置指標。參見Table 12.3. Other Ways to Define and Change shared_ptrs 表12.3 :定義和變更shared_ptr的其他方式:shared_ptr<T>p(q)

此例中shared_ptr乃區域性的,故會被銷毀,而使p無效,成為懸置指標。

參考計數(reference count)是用use_count()成員函式來取得

使用下列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

13:14

若執行下列程式碼,會發生什麼事?

auto sp = make_shared<int>();

auto p = sp.get();

delete p;

//此行可考慮改用成下式來釋放p

sp.reset(p);//俟考

https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise12_13/prog1/prog1.cpp

普通指標p的底層物件被刪除了,sp會成為懸置指標!且無法再進行=nullprt的運算了!又不能用make_shared回傳的,須如下式expression:

shared_ptr<int> sp(new int(1));

才能對p使用delete

在Visual Studio 2019執行到 delete p;此行時會丟出例外情形:

prog1.exe has triggered a breakpoint. occurred

應該是因為用delete要與new配合,p並不是new出來的,就不能用delete。是否?應該是的!14:30



如果我們執行下列程式碼,會發生什麼事?

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. Smart Pointers and Exceptions

12.1.4智慧指標和例外情形

在§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的過程中這個記憶體就會得到釋放。

然而,若是直接管理動態記憶體,而不是經由智慧指標來代管的話,一旦例外狀況發生,這個被直接管理的記憶體並不會因此得到釋放。也就是說,若我們是使用內建的普通指標而非智慧指標來直接管理記憶體,那麼當例外情形發生在一個new後、且在這個new對應的delete前,因為尚未執行delete操作,程式就被迫中斷,所以那個記憶體就不會得到釋放:

頁468

void f()

{

int *ip = new int(42);//動態配置一個新的物件

//這裡的程式碼會丟出一個沒有在f內被捕捉到的例外

delete ip; //在退出f區塊前釋放記憶體

}

像這樣,如果例外發生在new和delete之間,而且未在f內就被捕捉到,那麼這個由new配置allocate出來的記憶體資源就再也沒有機會被釋放了。因為在函式f外並沒有指向這個記憶體位址的指標存在,因此,這個記憶體就無從管控、也就無法得到釋放。

Smart Pointers and Dumb Classes

智慧指標和愚類別(Dumb Classes)

智慧指標和啞類別、呆類別(Dumb Classes)就是死掉了它也不會哀/哎

智慧指標和一種特殊的類別——窘類別(囧類別)(不智類別Dumb Classes)——會配置資源卻沒有定義好解構器(destructor)以釋放資源的類別

原來Smart和Dumb是相對的

就是智與不智,聰明與呆笨也

許多C++類別(應即內建型別builtin type),包括所有的程式庫類別,都已有定義了適當的解構器(destructors,§12.1.1)來負責清理其類別物件用過的資源。然而,並不是所有的類別都如此;特別是,那些想要讓C和C++可以兼容共用的類別,一般都會要求用到它們的程式碼要記得明確釋放它們用過的資源。

若使用那些會配置資源、卻缺乏適當解構器來釋放那些資源的類別,就很有可能會碰到我們在使用動態配置記憶體時常會碰到的錯誤——就是很容易忘了去釋放掉再也用不到的資源。同樣地,如果例外狀況發生在該資源配置之後、且在其被釋放以前,那麼這樣的程式在執行起來就極有耗漏記憶體資源的危險。

我們可使用管理動態記憶體的那套方式來管理缺乏完善解構器的類別。舉例來說,可以想像一下我們正在使用C和C++通用的network(網路)程式庫。使用這個程式庫的程式可能會包含像下面這樣的程式碼:

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)關掉。

這應也就是後面的文字查詢程式為什麼要用到shared_ptr的原因

connection

struct destination; // represents what we are connecting to

struct connection; // information needed to use the connection

connection connect(destination *); // open the connection

void disconnect(connection); // close the given connection

void f(destination &d /* other parameters */)

{

// get a connection; must remember to close it when done

connection c = connect(&d);

// use the connection

// if we forget to call disconnect before exiting f, there will be no way to close c

}

56:10

Using Our Own Deletion Code

使用我們自己的刪除物件程式碼指令

使用我們自訂的清除資源(Deletion)程式碼

shared_ptr都會默認它們指向的是動態記憶體。所以,當shared_ptr被摧毀殆盡時(參考計數歸零時),它就會在它所持有的普通指標上逕行delete運算。因此我們就可以利用它這樣的特性,來將如connection這般無智的智障的類別非智能的類別、低能類別(dumb classes)物件「偽裝」成在動態記憶體區配置的物件,讓shared_ptr來管控這樣類別的物件。

頁469

若要像這樣利用shared_ptr來管控connection類別物件,我們就必須先自行定義(自訂)一個函式來進行清除資源的工作,以取代delete。必須要能用儲存在shared_ptr內的指標來呼叫這個自訂的刪除器(deleter)函式,才能對這個shared_ptr所指向的物件進行刪除。因此在這裡,我們的刪除器就必須是一個有型別為connection*(對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);//這樣就把p與c繫結(bind)在一塊了,成了生命共同體—脣亡齒寒。這樣看來shared_ptr也是蠻狠的嘛→恐怖情人

// use the connection

// when f exits, even if by an exception, the connection will be properly closed//也就是當p消亡時也會拉c下水!

}

void end_connection(connection *p){ disconnect(*p); }

當我們創建一個shared_ptr,我們可以傳入一個選擇性的引數d來指向一個刪除器函式(§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正常結束,或因出現例外狀況而被迫中斷,這個連線仍會由p來負責銷毀,而得以切斷

}

當智慧指標p被摧毀時,因為建構它時引入了end_connection這個引數,它就不會在它所儲存的指標上調用delete來進行清除的動作,而是會用那個指標去呼叫end_connection這個刪除器(deleter)以進行資源的清理。在刪除器end_connection執行時,則會呼叫disconnect這個成員函式,這樣就可以確保連線會被妥善地關閉。如果f函式能夠正常結束,那麼區域變數智慧指標p也會在函式結束(return)時被摧毀。甚至,即使在執行f函式的過程中發生了意外例外,這個p也一樣會被摧毀,而p管控的連線也仍可得以關閉。能夠這樣,都是因為創建智慧指標p時,已同時帶入end_connection來作為清除資源的憑藉,其效用就等同於shared_ptr類別在預設情況會調用的delete。

Caution: Smart Pointer Pitfalls

注意:使用智慧指標須注意的事項

只有正確使用智慧指標,智慧指標才能發揮它該有的功能:讓控管動態配置物件(memory)及動態記憶體區(dynamic memory)的工作變得更為容易且可靠。若要正確使用智慧指標,我們就必須遵守以下原則:

•別用同一個內建型別的普通指標來初始化(或reset)多個智慧指標。

•別在get()回傳的普通指標上進行delete運算。

•別使用get()回傳的普通指標來初始化或reset另一個智慧指標。如此做也就類似第1條的一對多了—一個普通指標卻去初始化或指定給多個指標。

•如果非得要用到 get()所回傳的一個普通指標,那麼就要記得這個普通指標會在和它同一指向的智慧指標都消失後,變得無效,淪為懸置指標。因為智慧指標的參考計數歸0時,都會直接刪除它所指向的物件,不管還有沒有其他的普通指標指向著這個物件。

•雖然智慧指標是設計用來給管控動態配置記憶體時用的,但智慧指標也可以用來管理一般記憶體區上的物件。但若用智慧指標來管理那些非new配置出來的資源,我們就必須引入自訂的刪除器來取代智慧指標預設會使用的delete運算子。(§12.1.4,頁468、§12.1.5,頁471)。因為delete定是搭配new來使用的,現在不用new出來的資源,就不能再用delete來進行該資源的清除工作。

練習12.14

撰寫一個你自己版本的函式:一樣使用shared_ptr來管理connection。

暫略

寫出一個你自己版本的函式,用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);

}

1:7:27

練習12.15

改用lambda(§10.3.2),而不用函式end_connection,來改寫前面的練習。

改寫第一個練習,這次使用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);

}


頁470

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)在一塊【其實就是把這個new出來的普通指標作為unique_ptr的底層指標給安插進去;shared_ptr亦同原理】;也就是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))

表12.4 : uniqae_ptr的運算(參照表12.1)

unique_ptr<T> u1 unique_ptr<T,D> u2 u1、u2是unique_ptr,指向的物件其型別為T,二者尚未初始化,故其值為null(空指標)。u1和u2不同的地方在於:u1會使用delete來對其底層指標執行delete,以釋放記憶體資源;u2則會用一個型別為D的可呼叫物件來取代delete以釋放其所佔用的資源。Null unique_ptrs that can point to objects of type T. u1 will use delete to free its pointer; u2 will use a callable object of type D to free its pointer.

unique_ptr<T,D> u(d) u 是個unique_ ptr,指向了型別為T的物件,並使用d來取代delete運算。因u未經初始化,故其現值為null。d必須是型別為D的一個物件(object,這裡是臺語:東西的意思)。Null unique_ptr that point to objects of type T that uses d,which must be an object of type D in place of delete.

u = nullptr 設定u為null空指標,就删除了u所指的物件。因為unique_ptr的參考計數(reference count)恆為「1」! Deletes the object to which u points; makes u null.

u.release() 放棄(釋出、放手)u對其持有的普通指標的控制權(棄權);會回傳u所持有的普通指標,並使得u成為null空指標。也就是不必擔心u會因此成為懸置指標。Relinquishes control of the pointer u had held; returns the pointer u had held and makes u null.

u.reset()

u.reset(q)

u.reset(nullptr) 刪除u所指的物件。Deletes the object to which u points;

如果提供了內建指標q,就讓u改指向q所指向的物件。If the built-in pointer q is supplied, makes u point to that object.

否則u就會變為null空指標。Otherwise makes u null.

u.reset(nullptr)和前u = nullptr應是等效的。



雖然無法拷貝或指定一個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曾指向的記憶體,因為p2也是unique_ptr,所以當它不再指向原物件時,自然就會銷毀它曾有的禁臠

release成員函式回傳的是目前儲存在unique_ptr中的普通指標,並使那個unique_ptr的值成為null——也就是成了空指標nullptr。這裡(在這之前),p2會以曾在p1中的指標值來初始化,而p1在經過調用release的運算後,則會變為空指標。

頁471

1:36:35

reset成員函式則接受一個選擇性的普通指標引數,並重新將unique_ptr指向引數指標所指向的物件。如果這個unique_ ptr本來不是null,那它原指的物件就會在reset後被消除。在這裡對p2進行reset的呼叫,會釋放原來它以"Stegosaurus"這個string來初始化所佔用的記憶體;然後將p3原有的普通指標讓渡給p2,此時就會使p3的值轉為null了。

release的呼叫會切斷呼叫它的unique_ptr與它所托管物件之間的聯繫(其實release就是要釋出它的底層指標)。通常release回傳的普通指標會被用來初始化或指定給另一個智慧指標。在這種情況下,管理這個記憶體的責任就只是從一個智慧指標轉移到另外一個。然而,若我們沒將release回傳的指標儲存在一個智慧指標中,交它來管控,那麼我們的應用程式就必須負責運用這個release回傳的普通指標來處理相關資源的釋放工作(也就是不交給智慧指標來代理,就得自己負責去處理):

p2.release() ; //錯了:p2在經release後並不會釋放它所佔用的記憶體,我們若沒能把release回傳的普通指標(底層指標)給儲存下來,那麼就會失去對那個記憶體控制的能力(即遺失丟失指向該記憶體位址的指標)

auto p = p2.release ();//ok,但我們必須記得在p所指向的資源不再用到時,要對p進行delete運算,以釋放其記憶體資源

p2.release(); // WRONG: p2 won't free the memory and we've lost the pointer

auto p = p2.release(); // ok, but we must remember to delete(p)

總之,記憶體都是由指標來管控的,故不可隨意丟棄有用的指標

Passing and Returning unique_ptrs

unique_ptr的傳遞與回傳 1:45:20

「無法對unique_ptr進行拷貝」這個規則有一個例外:那就是我們可以拷貝或指定即將被摧毀的unique_ptr。因為它是「unique」(獨佔性的),只要不違反unique的原則下,當然都是可以的{懂得道理好修行}。既然是要被摧毀了,當然殘存下來的,依然就只有一個——也就是「unique」!最常見到的情形就是從一個函式回傳一個unique_ptr:

unique_ptr<int> clone (int p){

//ok:從int*明確創建一個unique_ptr<int>。雖然函式是以拷貝的方式回傳的,但這個unique_ptr區域變數也即將銷毀

return unique_ptr<int>(new int(p));

}

又或者,我們也可以回傳一個unique_ptr的區域物件的拷貝副本:

unique_ptr<int> clone(int p) {

unique_ptr<int> ret(new int(p));

//…

return ret;

}

編譯器當然知道在這兩種情況下,回傳的物件就要消逝。因此,它就會進行一種特殊的「拷貝」,使得這個unique_ptr不會與「unique」原則衝突;這會在§ 13.6.2(頁534)中再加以討論。

Backward Compatibility: auto_ptr

保持與舊版中auto_ptr的相容性

雖然auto_ptr還是標準程式庫的一部分,但應該盡量改用unique_ptr才好

早期版本的程式庫還有一個叫做auto_ptr的類別,它具有unique_ptr的部分屬性,但非全部——尤其是,無法將一個auto_ptr儲存在容器中、也不能將它從一個函式回傳拷貝出來。所以較諸unique_ptr,它的功能是很有限的。

雖然auto_ptr仍是標準程式庫的一員,我們要寫程式時還是該優先選用unique_ptr,而不要再用auto_ptr了!

Passing a Deleter to unique_ptr

將刪除器傳給unique_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)中再加以描述。

頁472

2:0:10

若要覆寫unique_ptr中的刪除器,那麼建構(或reset)unique_ptr型別物件的語法方式,就必須加以調整。類似於覆寫關聯式容器的比較運算(§11.2.2,頁425),我們必須在建構一個unique_ptr物件時,在宣告的角括號(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);

在角括弧內同時提供刪除器(deleter)的型別及unique_ptr可以指向的型別

// p points to an object of type objT and uses an object of type delT to free that object

// it will call an object named fcn of type delT

unique_ptr<objT, delT>p(new objT, fcn);


為了更具體地掌握這樣的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結束後——即使僅是因為發生例外狀況而被迫中止,連線都可以得到正確地關閉,其所佔用的資源也因此得到正確地釋放

}

將前connection的程式,改用unique_ptr來寫:

void f(destination &d /* other needed parameters */)

{

connection c = connect(&d); // open the connection

// when p is destroyed, the connection will be closed//因為p是unique參考計數(reference count)恆定為1

unique_ptr<connection, decltype(end_connection) *>

p(&c, end_connection);

// use the connection

// when f exits, even if by an exception, the connection will be properly closed

}

decltype參見頁70、250。decltype回傳的是函式型別(function type),而非對函式型別的指標,故後要再加「*」。

在這裡,我們可以看到使用了decltype(§2.5.3,頁70、250)來指定函式指標型別。因為decltype(end_connection)回傳的只會是一個精準的函式型別,而不是對函式的指標(function pointer),所以我們必須記得在其後再加上一個「*」來表示我們要用的是對該函式型別的一個指標(§6.7,頁250)。

練習12.16

若企圖拷貝或指定一個unique_ptr,編譯器出現的錯誤訊息通常是很複雜的。寫一個含有這樣錯誤的程式來測試看看,你的編譯器所顯示的診斷資訊會是什麼。

當我們試著對一個unique_ptr物件進行拷貝或指定操作時(除了將之指定為空指標nullptr),編譯器並不會給出很明確的錯誤資訊。試著寫一個包含了這樣錯誤的程式,看看你的編譯器會出現怎麼樣的診斷資訊。

using namespace std;



int main() {

unique_ptr<int>up(new int(12));

unique_ptr<int>up1=up;

}



Severity Code Description

Error (active) E1776 function "std::unique_ptr<_Ty, _Dx>::operator=(const std::unique_ptr<_Ty, _Dx> &) [with _Ty=int, _Dx=std::default_delete<int>]" (declared at line 1915 of "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.23.28105\include\memory") cannot be referenced -- it is a deleted function



Severity Code Description

Error (active) E1776 function "std::unique_ptr<_Ty, _Dx>::unique_ptr(const std::unique_ptr<_Ty, _Dx> &) [with _Ty=int, _Dx=std::default_delete<int>]" (declared at line 1914 of "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.23.28105\include\memory") cannot be referenced -- it is a deleted function

練習12.17

2:7:19

下列哪些unique_ptr的宣告是合法的,或可能會出現錯誤;並請解釋每個錯誤的問題所在。

using namespace std;



int main() {

int ix = 1024, * pi = &ix, * pi2 = new int(2048);

typedef unique_ptr<int> IntP;

//IntP p0(ix);//(a) :不能直接用int來初始化

IntP p2(pi2);//(c) :要用new回傳的pointer

//IntP p1(pi);//(b)竟然連普通取址運算子回傳的指標也可以。只有在編撰時才行;若執行,仍會出錯!參見下面練習12.17 :

//IntP p3(&ix);//(d)和(b)是一樣的:prog1.exe has triggered a breakpoint.occurred

//IntP p4(new int(2048));//(e)和(c)一樣

IntP p5(p2.get());//詳見後文


練習12.17 :

}



下列哪些程式碼對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

為什麼shared_ptr就沒有release成員呢?

shared_ptr就是一直share出去的,不怕重複、複本,當然就不必release(釋出、放手)了,它們本來就是要與它人共享了,何勞放手、釋出呢?而unique_ptr因為是unique,只能有一個對應到它所指向的動態配置記憶體,所以才必須有release將其所指向的記憶體釋出才能來作轉移。

為什麼shared_ptr沒有release成員呢?

就如同「unique」不能違反其「獨佔」的原則,既然叫「shared」還有什麼好「release」的呢?都已經「shared」了,還有什麼好「release」的呢?根本就是彼此矛盾、衝突、不相容的屬性了。



留言

熱門文章