c++動態記憶體智慧指標及weak_ptr用法的理解

TinnCHEN發表於2019-04-10

靜態記憶體

1、靜態記憶體:用來儲存區域性static物件(第一次經過時初始化直到程式終止才銷燬,貫穿函式呼叫及之後的時間)、類static資料成員(與類本身相關與類物件無關,不能為const,不包含this)以及定義在任何函式之外的變數。

2、棧記憶體:用來儲存定義在函式內的非static物件。

3、對於1和2,由編譯器自動建立和銷燬。

智慧指標shared_ptr

4、為了避免使用動態指標帶來的風險(記憶體洩漏或者產生引用非法記憶體的指標),推薦使用智慧指標。

5、shared_ptr類 shared_ptr允許多個指標指向同一個物件,宣告方式類似vector。

6、最安全的分配和使用動態記憶體的方法是呼叫一個名為make_shared的標準庫函式。此函式在動態記憶體中分配一個物件並初始化它,返回指向此物件的shared_ptr。

7、幾種使用make_shared方式

//指向一個值為42的int的shared_ptr
shared_ptr<int> p1 = make_shared<int> (42);
//指向一個值為”9999999999“的string
shared_ptr<int> p2 = make_shared<string>(10, "9");
//指向一個值初始化的int,即,值為0
shared_[tr<int> p3 = make_shared<int>();
//指向一個動態分配的空vector<string>
auto p4 = make_shared<vector<string>>();

8、我們可以認為share_ptr有一個關聯的計數器,通常稱作引用計數,來確保當其為0時釋放空間。當我們進行拷貝或者賦值時,每個shared_ptr都會記錄有多少個其他shared_ptr指向相同的物件。

9、造成shared_ptr的計數器遞增的情況:(1)當用一個shared_ptr初始化另一個shared_ptr(2)將它作為引數傳遞給一個函式(3)作為函式的返回值

10、造成shared_ptr遞減的情況:(1)給原有的shared_ptr賦予新值(2)shared_ptr被銷燬,比如說一個區域性的shared_ptr離開其作用域。

11、當有多個shared_ptr指向同一塊動態記憶體時,即使其中某個被銷燬也不會釋放此記憶體,直到最後一個shared_ptr被銷燬時才釋放記憶體。因此我們要保證shared_ptr在無用之後不再保留。比如說,你將shared_ptr存放於某一個容器中,而後不再需要全部元素,而只使用一部分元素,要記得用erase刪除不再需要的元素。

12、程式使用動態記憶體出於以下三個原因之一:(1)程式不知道自己需要使用多少物件eg:容器類(2)程式不知道所需物件的準確型別(3)程式需要在多個物件間共享資料。

直接管理記憶體

13、我們也可以採用new和delete來直接管理動態記憶體,但風險比較大,且沒有智慧指標所提供的相關操作。

14、new無法為其分配的物件命名,而是返回值一個指向該物件的指標

int *pi = new int;
int *pt = new int(); //值初始化為0
int *p = new int(42);
string s = new string; //類型別物件將使用預設建構函式進行初始化,s為空string
vector<int> *pv = new vector<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

預設情況下,動態分配的物件時預設初始化的(內建型別定義於任何函式體外的量初始化為0,定義在函式體內的內建型別的值為定義),這意味著內建型別或組合型別的物件的值是未定義的,因此我們可以對其採用值初始化的方式來初始化一個動態分配的物件。

15、一個動態分配的const必須進行初始化。

const int *pci = new const int(1024);

17、記憶體耗盡:如果new不能分配所要求的記憶體空間,它會丟擲一個型別為bad_alloc的異常。我們可以改變使用new的方式來阻止它丟擲異常:

int *p = new (nothrow) int; //如果分配失敗,new返回一個空指標

該形式的new稱為定位new

18、delete包含兩個操作(1)銷燬給定的指標指向的物件(2)釋放對應的記憶體

19、以下delete行為的後果是未定義的

int i, *pi1 = &i;
double *pd = new double(33), *pd2 = pd;
delete i; //錯誤 i 不是指標
delete pi1; //未定義:pi1指向一個區域性變數
delete pd;
delete pd2; // pd2指向的記憶體已經被釋放 

20、常見的三個錯誤(1)忘記delete記憶體,也就是記憶體洩漏(2)使用已釋放的物件(3)同一塊記憶體連續釋放兩次。

21、空懸指標:指向一塊曾經儲存資料物件但現在已經無效的記憶體的指標。常見於delete一個指標後,很多機器仍保留著(已經釋放的)動態記憶體的地址。我們可以在delete之後將nullptr賦給該指標,顯示的指出該指標不指向任何物件。

shared_ptr與new結合

22、必須採用直接初始化形式用new來初始化shared_ptr,因為接受指標引數的智慧指標建構函式為exclicit,必須顯示的宣告。並且用於初始化智慧指標的普通指標必須指向一個動態記憶體。

shared_ptr<int> p1 (new int(1024));

22、部分操作列舉:

shared_ptr<T> p(q,d); //p接管了內建指標q所指向的物件的所有權。q必須能轉化為T*型別。
                     //p將使用可呼叫物件d來代替delete

//若p是唯一指向其物件的shared_ptr,reset會釋放此物件。
//若傳遞了可選的引數內建指標q,會另p指向q,否則會將p置空。
//若還傳遞了引數d,將會呼叫d而不是delete來釋放q
p.reset();
p.reset(q);
p.reset(q,d);

23、儘量不要混用new和shared_ptr,因為shared_ptr的析構僅限於shared_ptr之間,這也是推薦使用make_sahred而不是new的原因。

24、當混用了new和shared_ptr時,一旦我們將一個shared_ptr繫結到一個普通指標上,我們就將記憶體的管理責任交給了shared_ptr,之後我們就不應該再使用內建指標來訪為shared_ptr指向的物件了。

25、不要用get初始化另一個智慧指標或為智慧指標賦值。(get僅被用於我們需要向不適用智慧指標的程式碼傳遞一個內建指標。使用get返回的指標的程式碼不能delete此指標)

26、reset經常與unique一起使用來控制多個shared_ptr共享的物件。用來判定我們是否是當前物件僅有的使用者,若不是,在改變之前需要製作一份新的拷貝。

智慧指標和異常

27、使用智慧指標的一些基本規範:
(1)不使用相同的內建指標初始化或reset多個智慧指標。
(2)不delete get() 返回的指標。
(3)不使用get() 初始化或reset另一個智慧指標。
(4)如果使用了get() 返回的指標,記住當最後一個對應的智慧指標銷燬後,這個返回的指標也無效了。
(5)如果智慧指標管理的資源不是new分配的記憶體,記得傳遞給它一個刪除器。

unique_ptr

28、當我們定義一個unique_ptr時,需要將其繫結到一個new返回的指標上。必須採用直接初始化形式。不支援普通的拷貝或者賦值操作。

29、一些基本操作

u.release(); //u放棄對指標的控制權,返回值真,並將u置空
u.reset();//釋放u指向的物件
u.reset(q);//如果提供了內建指標q,令u指向這個物件,否則將u置空
u.reset(nullptr);

//我們可以用release和reset來將指標的所有權從一個(非const)unique_ptr轉移給另一個unique
unique_ptr<string> p2(p1.release());//release將p1置空,將所有權轉移給p2
unique_ptr<string> p3(new string("Text"));
p2.reset(p3.release());//將所有權從p3轉移給p2,並且釋放了p2原來指向的記憶體。

30、例外:我們可以拷貝或賦值一個將要被銷燬的unique_ptr

unique_ptr<int> clone(int p){
          return unique_ptr<int>(new int(p));
}

//or
unique_ptr<int> clone(int p){
          unique_ptr<int> ret(new int(p));
          return ret;
}

weak_ptr

31、weak_ptr指向由一個shared_ptr管理的物件,不控制物件生存期,不會改變shared_ptr的引用計數,當最後一個shared_ptr被銷燬,物件就會被釋放。

32、部分weak_ptr操作

w.use_count();//與w共享物件的shared_ptr的數量
w.expired();//若w.use_count返回0,返回ture,否則返回false
w.lock();//如果expired返回true,返回一個空shared_ptr,否則返回一個指向w物件的shared_ptr

33、weak_ptr解決了在迴圈引用中的記憶體洩露的情況,以及懸空指標的情況。

//懸空指標
shared_ptr<int> sptr(new int(10));  
weak_ptr<int> wptr1 = sptr;   
sptr.reset(new int(20));    
if(auto tmp = wptr1.lock())  //lock()返回shared_ptr,非空則輸出
     cout << *tmp << '\n';
else
     cout << "expired!" << '\n';

迴圈引用:
在這裡插入圖片描述
虛線代表weak_ptr,實線代表shared_ptr
當我們函式執行快結束時,此使指向B的shared_ptr有兩個,指向A的有一個,因此在函式結束時,指向A的引用計數-- ,變為0,正常釋放物件A,同時,A指向B的shared_ptr也被釋放,此時,B的引用計數為1-- ,於是物件B也正常釋放。
假設B指向A的也為shared_ptr,我們可以看到在函式即將結束時,兩個物件引用計數都為2,無法正常釋放記憶體,因此也就造成了記憶體洩漏。

相關文章