千慮一得齋OnLine_觀死書齋-Yahoo/Hexun/Blogger/sina/Xuite

觀死書齋暨Spread、和訊博客全文檢索

2019年12月18日 星期三

C++自修入門實境秀、C++ Primer 5版研讀秀 81 ~ 12.1. Dynamic Memory and Smart Pointers...





來幫中文版改作文囉:

我們目前寫過的程式,用的都是定義了明確生命長度(生命週期lifetimes)的物件(objects)。全域性的物件會在整個應用程式啟動時先行配置妥當,並在程式結束時結束。區域性生成的物件則會在程式執行點進入它們所在的區塊時生成,並在離開該區塊時結束。由應用程式靜態配置的(static)區域性物件,則會在初次使用到它們前配置妥當,而在整個應用程式結束時才會結束。

除了支援區域生成性的物件和應用程式靜態配置的物件,C++也能讓我們自行動態配置物件(機動配置物件)。動態配置的物件打從它們被創建時便具有獨立(lifetimes,生命週期),它們的生命長度會一直持續到明確釋放它們為止。

正確地釋放動態配置物件已知是非常常見的臭蟲來源。為了能更妥善地利用動態配置物件,C++的程式庫因此定義了兩種型別的智慧指標,來有效管控動態配置的物件。這兩種型別的智慧指標都能在適當的時機下釋放它們所指向的動態配置物件所佔用的記憶體。



改中文版作文:

我們到目前為止練習的程式,對於系統記憶體的使用,只用到靜態的(static)和堆疊式(stack)這兩種記憶體區域。靜態記憶體是用來配置靜態的(static)物件,至少包括:區域性的靜態(static)物件(§6.1.1)、類別的靜態(static)資料成員(data members,§7.6),以及在任何函式外定義的變數,這3種。堆疊式記憶體則是用來配置非靜態的(static)物件,如定義在函式內的非靜態(static)物件(也就區域性生成的物件,即所謂的自動物件automatic object)即是。在這兩種記憶體區域(即靜態和堆疊式記憶體)中配置的物件會由編譯器來自動創建並予摧毀。至於堆疊式記憶體配置存放的物件則僅僅在它們被定義的區塊還在執行的時候才有效、才存在;然而若是靜態配置的(static)區域性物件,這樣的物件就會在它們第一次被使用前配置好,而只會在整個應用程式(program)結束時才被摧毀。

除了上述兩種記憶體區域(靜態與堆疊記憶體)外,作業系統對每個應用程式(program)還都配有一個叫做集區(pool,在這裡應該有點蓄水池的意思)的記憶體可資使用。這個記憶體區域也叫作自由存放區(free store)或heap(堆積記憶體區)。應用程式則會把這個系統配置給它的堆積區域(heap)供給它所需要用到的動態配置(dynamically allocate)物件來使用。這種物件是程式在執行期間(run time)所動態進行配置的物件,因此程式本身就有責任去好好管控這些動態物件的生命長度,所以我們在撰寫這樣的程式它的程式碼的時候,就必須在不再需要這種物件的時候明確下達摧毀它們的指令,以釋放相關的記憶體。

警告:儘管有時非得用上動態記憶體(dynamic memory),但眾所周知,動態記憶體的難管控是出了名的。(可以說動態記憶體是桀敖不馴的,很難馴服的。)

12.1動態記憶體與智慧指標

在C++中,動態記憶體是透過一對運算子來管控的:new運算子能在動態記憶區中配置一個物件,也可藉由它來初始化該物件。在配置妥當後,會傳回一個指向該物件的指標;而delete運算子則帶有一個指向動態物件指標的參數,可以藉以摧毀該動態物件,並釋放它所關聯到的記憶體。

動態記憶體之所以桀敖不馴、難以管控,是因為釋放記憶體正確的時機,是很難掌控的。通常不是忘了釋放記憶體,而導致了記憶體資源的浪費——即所謂的記憶體洩漏(memory leak);就是不小心誤將仍有指標指向它們的記憶體給釋放了,在這種失誤下,就會有指標指向不再有效的記憶體位置。

為了讓動態記憶體的使用變得為更容易(也更加安全),新的程式庫提供了兩種智慧指標(smart pointer)型別來管理動態物件。智慧指標的表現其實就跟一般指標沒什麼兩樣,只不過有一個重大的特性:那就是,它會在適當的時機自行刪除它所指向的物件。新的程式庫定義了兩種智慧指標,它們的差別在於管理其底層指標的方式有所不同:shared_ptr這樣的智慧指標可容許多個底層指標指向相同的物件,而unique_ptr這種智慧指標,則會「獨佔(owns)」它所指向的物件,不與其他共用。程式庫還定義了一個與這兩種智慧指標相伴的類別,名為weak_ptr,它是一種弱參考型的(weak reference)指標 ,指向由一個shared_ptr管控的物件。這三種智慧指標全都定義在memory這個標頭檔中。

12.1.1 shared_ptr 類別

就像vector,智慧指標是一種模板(templates,§3.3,p96)。因此,創建一個智慧指標的時候,我們必須提供一個額外的資訊,這個資訊就是要創建的指標想要指向的型別。跟定義一個vector一樣,我們會在要定義的智慧指標型別名稱之後,加上角括號來提供這個型別:

shared_ptr<string> p1; // 可以指向一個 string的 shared_ptr

shared_ptr<list<int>> p2; // 可以指向由 int 組成的一個 list 的 shared_ptr



改中文版作文:

4:20:39經過預設初始化後的智慧指標,會是一個null指標(§2.3.2,頁54)。在§12.1.3(頁464)中,我們才會討論到初始化一個智慧指標的其他方式。

使用智慧指標的方式就如同使用一般的指標。因此,解參考(dereferencing)一個智慧指標就會回傳該智慧指標指向的那個物件。當我們在一個條件句中使用到一個智慧指標,其效果就等同於測試該指標是否為 null :

//如果p1不是null,檢查它是否為一個空string

if (p1 && p1->empty())

*p1 = "hi"; //若是空string,就將p1解參考,並指定新值”hi”給解參考的結果

表12.1列出了 shared_ptr和unique_ptr共通的運算。專屬於shared_ptr的那些則列於表 12.2。

make_shared 函式

配置並使用動態記憶體最安穩的方式就是呼叫名為make_shared的程式庫函式。這個函式會在動態記憶體區中配置並初始化一個物件,然後回傳一個shared_ptr指向該物件。和智慧指標一樣,make_shared也是定義於memory的標頭檔中。

當我們呼叫make_shared,我們必須指定要讓make_shard創建出來的物件它的型別。指定的方式,就如同使用模板類別的方式,須在make_shared的函式名稱後接著一對角括號,並在其中指定該型別:

// p3指向值為 42 的一個 int

shared_ptr<int> p3 = make_shared<int>(42);

// p4指向一個string,其值為9999999999

shared_ptr<string> p4 = make_shared<string>(10, '9' );

// p5指向一個int,其值被值初始化( §3,3.1)為0

shared_ptr<int> p5 = make_shared<int>();

就像循序容器的emplace成員一樣(§9.3.1),make_shared也會利用它的引數型別來建構一個特定型別的物件。比如說,對make_shared<string>的呼叫,就必須傳入一個能與string類別定義的建構器匹配的引數。而對make_shared<int>的呼叫則可以傳入能夠用來初始化一個int的任何值,來作為make_shared<int>的引數。如果我們沒有傳入任何引數給make_shared,那麼make_shared所創建出來的物件就會是一個角括弧內所指定的型別經過值初始化後的(§3.3.1)物件。

不過一般說來,我們都會使用auto ( §2.5.2)來定義一個物件,以存放make_shared回傳的結果,這樣會更容易一些:

// p6指向一個動態配置的、空的vector<string>

auto p6 = make_shared<vector<string>>();

拷貝和指定shared_ptr

當我們拷貝或指定一個shared_ptr這樣型別的智慧指標的時候,每個參與拷貝或指定的shared_ptr都會同時追蹤記錄到底有多少shared_ptr指向相同的動態物件:

auto p = make_shared<int> (42) ; // p 所指的物件有一個使用者(即有一個shared_ptr指向它)

auto q (p) ; // 用智慧指標p將q初始化,則p和q會同時指向相同的動態物件

//則 p和q所指的物件此時有兩個使用者(即兩個智慧指標指向它) 5:12:30

改中文版作文:

表 12.1 : shared_ptr 和 unique_ptr 共通的運算

shared_ptr<T> sp

unique_ptr<T> up 宣告了一個可指向T型別的物件,而其值為null的shared_ptr或unique_ptr智慧指標。

p 在條件句中使用p,如果p有指向一個物件,其值為true。



*p 將p解參考來取得p所指向的物件。



p->mem 與(*p).mem等效。



p.get() 回傳智慧指標P中的一般指標 。使用時請小心,此操作下回傳的指標所指的物件可能會在智慧指標p刪除該物件的時候消失。

swap(p,q) p.swap(q) 將智慧指標p與q中的指標對調。即對調p與q此二變數所載(所存放)的指標

可以將一個shared_ptr想成它有一個關聯的計數器,這個計數器通常會被稱作參考計數(reference count)。每當一個shared_ptr被拷貝的時候,這個計數就會遞增。舉例來說,與一個shared_ptr關聯的計數器會在以下情況中遞增其值:

1.當我們用它來初始化另一個shared_ptr的時候、2.用它來作為指定式的右運算元時,3.我們以傳值的方式將它傳入函式(§6.2.1)或從函式回傳(§6.3.2)其值的時候。而這個計數器會在以下的情況下遞減其值:1.指定一個新值給shared_ptr時,2.shared_ptr本身被摧毀時,比如當程式的執行點超出了一個區域性的shared_ptr的範疇(§6.1.1)的時候,這個區域性的shared_ptr的生命週期自然就結束。

只要一個shared_ptr這樣關聯的計數器其值降為零,這個shared_ptr就會自動釋放它所管控的動態物件(在動態記憶體區中所配置的物件):

auto r = make_shared<int>(42); // r 所指的 int 有一個使用者,其參考計數(reference count)此時為1

r = q; //指定給r,讓它指向一個不同的位址

//遞增q所指物件使用的數量

//遞減r曾指過的那個物件使用的數量,

//r曾指的那個物件現在沒有使用者了,那個物件會自動被釋放

這裡我們配置了一個int,並將對那個int的一個指標儲存到r中。接著,我們指定一個新的值給r。而r是指向我們之前配置的那個int物件唯一的 shared_ptr。那個int物件會在指定 q給r的過程中自動被釋放。

注意:是要採用一個計數器還是其他的資料結構來追蹤記錄智慧指標指向同一物件的個數,這是由實作自己來決定的。不論如何,其關鍵在於,類別必須要記下尚有多少shared_ptr指向同一物件,並在適當的時候自動釋放那個物件。

shared_ptr會自動摧毀它們的物件…

當指向一個物件的最後一個shared_ptr被摧毀時,shared_ptr類別就會自動摧毀那個 shared_ptr所指的物件。它會假手另一個特殊的成員函式來進行這樣的操作,這種的特殊的成員函式叫作解構器(destructor)。每個類別有建構器,也都會有一個解構器。建構器會控制該類別型別物件如何被初始化,而解構器則會控制該類別型別的物件在被摧毀的當時將會發生什麼事。



改中文版作文:

表12.2 : shared_ptr專屬的運算

make_shared<T>(args) 回傳一個shared_ptr指向一個動態配置的T型別物件,且使用 args來初始化那個物件。

shared_ptr<T> p(q) shared_ptr智慧指標 p是shared_ptr q的一個拷貝,在這樣的操作下會遞增q中的參考計數。q中的一般指標必須能夠轉換為對T型別的指標(即T*) ( §4.11.2,頁162)。



p = q shared_ptr p與q 存放的指標是可以互相轉換的。在這樣的指定式下會遞減p的參考計數並遞增q的;如果p的計數降到了0,就會刪除p指向的物件,而釋放其記憶體。



p.unique() 如果p.use_count()是1,就回傳true,否則為false。



p.use_count() 回傳與p共享物件的指標數量;可能會是冗長的運算,主要是用在除錯上。

類別的解構器會釋放已配置給其型別物件的資源。比如說,string建構器(以及其他的string 成員)會進行記憶體的配置來存放構成string的字元。string的解構器則會釋放那些已配置給該string字元用的記憶體資源。同樣地,有些vector的運算或操作也會配置記憶體來存放vector要用到的元素。而vector的解構器則會摧毀那些元素,並釋放那些元素所佔用的記憶體。

shared_ptr的解構器會遞減shared_ptr的參考計數。如果這個計數降為零,shared_ptr解構器就會摧毀那個shared_ptr所指的物件,並釋放該物件佔用的記憶體。

…並自動釋放關聯的記憶體

因為「shared_ptr類別會自動釋放不再需要的動態物件」,所以對動態記憶體的妥善使用就變得容易多了。舉例來說,我們可能會有一個函式回傳一個shared_ptr指向一個動態配置的物件,而這個動態物件(dynamic object)型別為Foo,且可由T型別的引數來將之初始化:

// factory會傳一個shared_ptr指向一個動態配置的物件

shared_ptr<Foo> factory (T arg)

{

//適當地處理arg

// shared_ptr會負責刪除這個記憶體

return make_shared<Foo>(arg);

}

因為factory會回傳一個shared_ptr,我們可以確定factory所配置的物件會在適當時機被釋放。舉例來說,下列的函式會將factory所回傳的shared_ptr儲存在一個區域變數中:

void use_factory(T arg)

{

shared_ptr<Foo> p = factory(arg); //使用p

} // p離開了其範疇,p所指向的記憶體會自動釋放



改中文版作文:

因為p對use_factory來說是區域性的,它會在use_factory結束時被摧毀(§ 6.1.1 )。當p被摧毀,它的參考計數會遞減,並受檢查。在此,p是指向factory所回傳的記憶體的唯一物件。因為p即將消失,p所指的物件也會被摧毀,而那個物件所佔用的記憶體會被釋放。

如果有其他的shared_ptr指向它,那個記憶體就不會被釋放:

shared_ptr<Foo> use_factory(T arg)

{

shared_ptr<Foo> p = factory(arg);

//使用p

return p; //我們回傳p的時候參考計數會遞增

}// p離開範疇了,p所指的記憶體卻並沒有被釋放

在這個版本中,use_factory中的return述句會回傳p的一個拷貝給其呼叫者(§6.3.2)。拷貝一個shared_ptr會遞增對那個物件的參考計數。而如今,p雖然被摧毀,但是p所指的記憶體卻仍有另外一個使用者指向它。shared_ptr類別會保證只要還有任何的shared_ptr依附在那個記憶體上,那個記憶體就不會被釋放。

也就是因為直到最後一個shared_ptr消失之前,shared_ptr所指向的記憶體都不會被自動釋放。因此我們在撰寫程式碼時,一定要顧慮到:當不再需要某個動態物件後,一定要把所有指向該物件的shared_ptr清除乾淨。若是忘了徹底清除用不到的shared_ptr,雖然應用程式仍能正常執行,沒有異狀,但卻會平白浪費掉可用的記憶體資源。在用完shared_ptr之後,會忘了清除乾淨,最有可能是發生在將 shared_ptr作一種容器的元素型別來使用,當對該容器元素進行了重新排序後,若不再需要容器中所有的元素,就應該把不再用到的元素確實進行容器的erase運算,以徹底清除那些用不到的shared_ptr元素。

注意:如果你將shared_ptr放到一個容器中,且你接著只需要使用到其中的某些元素,而非全部的元素,那麼就請你務必要記得清除掉你不再需要用到的元素。

沒有留言:

張貼留言