要點彙總:1) #9 - 基類的析構函數必須是virtual,否則可能導致析構調用鏈斷層。
智能指針的使用:1)使用 普通指針/其他已存在的智能指針/其他已存在的普通指針,對當前創建的智能指針進行初始化。 (創建 指針 指向當前已有內存)
2)使用 make_shared 創建全新的內存區,然後創建一個全新的智能指針指向它。 (創建 內存空間 和 指針)
3)使用 智能指針的 reset 方法來進行 智能指針的 重定向 和 釋放 (重定向 和 銷燬)
#1 ===========================================================================================================================
智能指針
智能指針有兩種: shared_ptr 和 unique_ptr
這兩種指針都是模板類,因此原型爲 xxx_ptr<T>
(!)注:智能指針是用來管理堆內存的,不是作爲指針使用的
(!)智能指針是使用delete來釋放內存,所以釋放內存時的特性和delete一樣
#2 ===========================================================================================================================
(!)首先明確一點,智能指針“僅僅”用來“動態分配內存”,而不是用來當做指針使用!!!!
因此:
int i = 100; //或者其他類,比如 classA a;
shared_ptr<int> sp = make_shared<int>(i);
上述語句實際上是使用i的值作爲新分配內存的初始值。sp並沒有指向i,而是指向了新分配的堆,這個堆存放int型的數值100
上述語句不能算錯誤,但是需要理解 make_shared 動作實際上是取堆裏申請了內存的。
(可以這樣理解,make_shared相當於 new ,尖括號指定需要分配內存的類型名,小括號指定作爲拷貝構造傳遞給
類型名的值,如果不指定,那麼使用類型的默認構造)
同理:
classA *pa = new classA();
shared_ptr<classA> sp = make_shred<classA>(*pa); //使用classA的拷貝構造在堆裏創建一個新的對象,而不是使用pa指向的對象
====== EXAMPLE 1 =======
#include "stdafx.h"
#include <iostream>
#include <memory>
using namespace std;
class A{
public:
A() = default;
~A(){ ; }
public:
int myint=10;
};
int _tmain(int argc, _TCHAR* argv[])
{
A* pa = new A();
shared_ptr<A> sp = make_shared<A>(*pa);
sp->myint = 100;
cout << pa->myint << endl; //pa = 10 ,值並沒有改變,因爲它是用 new 分配的堆
cout << sp->myint << endl; //sp = 100,值和pa不一樣,因爲它是用 make_shared 分配的堆
getchar();
return 0;
}
注:小括號裏的內容必須與尖括號裏類型的某個構造函數相匹配
====== EXAMPLE 2 ======
#include "stdafx.h"
#include <iostream>
#include <memory>
using namespace std;
class A{
public:
A() = default;
~A(){ ; }
public:
int myint=10;
};
int _tmain(int argc, _TCHAR* argv[])
{
shared_ptr<A> sp = make_shared<A>(); //使用 A 的默認構造函數
cout << sp.use_count() << endl; //爲1
shared_ptr<A> sp1 = sp; //增加了sp指向內存區的計數
cout << sp.use_count() << endl; //爲2
cout << sp1.use_count() << endl; //爲2
sp1 = NULL;
cout << sp.use_count() << endl; //爲1
getchar();
//小結:可見智能共享指針的計數器是由系統自動管理的,具體內部實現可能是 靜態數據類型,這樣各個
// 實例操作的就是同一個數據,當然,可能會涉及到線程安全的問題,這時候就要加鎖,這也伴隨着
// 效率略低和死鎖的風向
return 0;
}
#3 ===========================================================================================================================
(!)在使用共享指針時,我們只需要操作指針對象,不要試圖去直接操作指針指向的內容。因爲當所有指針都銷燬的時候,動態分配的內存會被
自動釋放。
計數增加:
1)初始化一個shared_ptr
2)作爲參數傳遞給一個函數
3)作爲函數返回值
計數減少:
1)給shared_ptr新指向對象,替換舊對象,那麼就對象計數減少
2)shared_ptr調用析構函數,比如局部變量離開作用域等等
小結:shared_ptr將對對象的內存管理轉變爲對對指向對象指針的管理。而這個指針的獲得僅能通過shared_make來實現。因此,從
某種意義上來說,更集中。
#4 ===========================================================================================================================
共享指針的應用場景:
1)程序不知道自己需要多少個對象???
2)程序不知道所需要對象的準確類型???
3)程序需要在多個對象之間共享數據,這有點像類的static成員變量,是所有類共享的。但是既然有static,爲什麼還要共享指針???
(!!!)使用動態內存的一個常見原因是:允許多個對象共享相同的狀態。
(!!!)爲什麼不做成static的?
主要因爲static數據存放在.bss區,無法手動釋放,在程序運行時就載入,而這部分的可用空間是有限而狹小的。如果我們想要使用堆棧這樣
寬敞的空間來模擬static成員,此時就可以使用共享指針管理的動態內存(堆)來實現,實現一個僅在大家都不在需要時纔會釋放的堆區域。
#5 ===========================================================================================================================
https://www.cnblogs.com/yinbiao/p/11563520.html
引用計數法的內部實現:
1)這個引用計數器保存在某個內部類型中,而這個內部類型對象在shared_ptr第一次構造時以指針的形式保存在shared_ptr中
2)shared_ptr重載了賦值運算符,在賦值和拷貝另一個shared_ptr時,這個指針被另一個shared_ptr共享
3)在引用計數歸0時,這個內部類型指針與shared_ptr管理的資源一起釋放
* 4)此外,爲了保證線程安全,引用計數器的加1和減1都是原子操作,它保證了shared_ptr由多個線程共享時不會爆掉
#6 ===========================================================================================================================
一個典型的場景:
class A{
public:
vector<classB> m_B_list; //非常龐大的一個列表(實際這裏可以使vector指針,這裏只是做一個描述)
}
如果A有N多個實例,問題就出現了,每個A實例都要有一個m_B_list,首先很喫內存,其次各個A實例之間需要實施互相同步數據,從而保證這個
vector對於所有A來說,都是一樣的。這是很棘手和難處理的:
1)可以讓vector是static的,但是這樣比較佔用static區域,而且無法釋放 (不可選)
2)可以簡單粗暴,讓所有A在更改vector之後,通知其他A跟新自己的vector,這樣既 佔內存,又 佔CPU (不可選)
3)創建一個command管理類,讓command管理類來存放vector,並提供增刪改查接口給所有A使用,這樣保證了效率,一定程度上節約了內存。
但是,需要新增一個類,而這個類的存在感很低,因爲它只是來做一箇中轉 (不是最優解)
4)不創建command管理類,但是在外部區域創建vector,然後讓A都能訪問,這樣有風險,如果某個A出於“某種原因”把vector給刪掉了。
那麼其他A在訪問的時候將訪問到空指針,或者直接崩潰。這種情況特別是在A都存放vector指針是明顯,因爲析構時會釋放成員變量。(不是最優解)
*5)使用shared_ptr,vector不使用new創建,而是使用shared_make,在A構造的時候創建。但是需要注意各個A運行在不同的線程下,同時
操作vector可能產生競爭。
第一種實現爲 =============
---A.h---
class A{
public:
A();
A(shared_ptr<vector<classB>> tmplist);
public:
shared_ptr<vector<classB>> m_p_B_list;
}
---A.cpp---
A::A():m_p_B_list(make_shared<vector<classB>>()){ //默認構造,列表初始化
}
A::A(shared_ptr<vector<classB>> tmplist):m_p_B_list(tmplist){
}
(???)上面的實現雖然使用了共享指針管理動態內存,但是多個A如果都使用默認構造的話,實際上還是創建了多個vector實例,然後各個A
管理自己的vector
#7 ===========================================================================================================================
動態分配const對象是合法的,new const string("xxxx");
按照const對象的屬性,const必須在定義是初始化,因此這種情況下不能使用默認構造函數。也可以通過delete來釋放分配的內存
(???)疑問:const類型變量一般存放在 .data段 。那麼動態分配的常量是否 也在其中。如果不是,是否存放在堆內,如果是在堆內,那麼這塊
區域是如何增加讀寫權限的,畢竟const類型只讀不可寫。
#8 ===========================================================================================================================
內存耗盡:
int *p = new (nothrow) int; //如果內存耗盡,那麼不拋異常bad_alloc,只是返回空指針
注:默認情況下,c++中,內存耗盡會拋出異常bad_alloc
#9 ===========================================================================================================================
delete接收一個指針,指針要麼指向一個對象,要麼是空指針。
delete動作會執行以下兩步:1)調用指針指向對象的析構函數;
2)釋放指針指向的內存。
如果指針類型和指向的內容不一致,比如 void* p = new classA();
==== 例子 ====
class A{
public:
A(){}
~A(){ cout << "~A()"; }
};
class B :public A{
public:
B(){}
~B(){ cout << "~B()"; }
};
int _tmain(int argc, _TCHAR* argv[])
{
//A *p_A = new A();
void *p_B = new B();
//delete p_A;
delete p_B;
getchar();
return 0;
}
小結:1)delete只認指針類型,如果指針是void類型,“那麼不會調用任何析構函數”。
2)同理,如果有父子關係,那麼delete父類指針,“只會調用父類的析構函數”。
(*) 3)爲了避免1)和2)中的問題,基類的析構函數必須是 “virtual” 的。
4)不論delete接收的是子類的指針還是基類的指針。類實例都會被完整地釋放掉,delete是跟着實例走,只認地址,而一旦涉及
到地址部分,malloc和free便不再理會指針類型,而是從操作系統層面完成內存的釋放。
(!!!)注:上面說到了 “基類的析構函數必須是virtual,否則在delete指向子類的基類指針時,會出現子類析構不會
被調用的問題”。如果子類中沒有需要回收的內存(通過new和malloc分配的),那麼內存是安全的,子類實例
會被正確地釋放。但是如果子類中進行了動態內存分配(new,malloc),那麼這部分內存就無法再被回收,至此
會導致內存泄露。
詳細描述:https://blog.csdn.net/u013797029/article/details/41848407
#10 ===========================================================================================================================
野指針:野指針是指,指針指向的內存已經被釋放掉了,但是指針沒有置空。使用delete操作指針並不會觸發指針的置空,因此在delete釋放
完內存以後,需要手動把指針置空,因爲內存區域已經不存在了,此時使用指針去訪問就會觸發異常 ---> "非法內存區訪問"。
#11 ===========================================================================================================================
可以在delete後立即置空指針來避免野指針的出現。但是如果某塊內存區域被多個指針指向,那麼就很容易忽略某個指針的置空操作。
因此:但凡涉及到多個指針指向同一塊內存區域的場景,“ 務必使用智能指針 ”來管理內存,此時我們不需要調用delete,只需要管理指針
即可,當所有的指針都置空,則內存被釋放。
#12 ===========================================================================================================================
上面提到了智能指針的使用方法,通過make_shared來分配內存,但是如果想使用new來分配內存,然後交由智能指針管理,該如何操作???
首先,智能指針的構造函數時explict的,即不接受隱式轉換,即必須是指定類型,故先new在通過賦值構造是行不通的。
可以使用複製構造函數來實現:
shared_ptr<classA> p_A = new classA(); //錯誤
shared_ptr<classA> p_A(new classA()); //正確
同理,使用普通指針給智能指針賦值也是行不通的:
shared_ptr<classA> clone(){ //錯誤
return new classA(); //返回值不接收這種隱式轉換
}
shared_ptr<classA> clone(){ //正確
return shared_ptr<classA>(new classA()); //返回值也是智能指針
}
(!!!)注:上面提到的使用普通指針來初始化智能指針的場景,要求普通指針指向的必須是動態內存(即new分配的),靜態內存不行,
比如:
int i = 10;
const int j = 10;
int *p = &i;
const int *pj = &j;
shared_ptr<int> sp(p); //不能把棧給智能指針
shared_ptr<int> spp(pj); //不能把data區給智能指針
#(!)13 ===========================================================================================================================
shared_ptr 和 unique_ptr 的構造和析構
classA A;
classA *p_A = &A; //創建一個實例給普通指針
unique_ptr<classA> up(new A()); //創建一個實例給unique智能指針
shared_ptr<classA> sp(new A()); //創建一個實例給智能指針
shared_ptr<classA> sp1(p_A); //讓智能指針指向p_A(普通指針)指向的對象,要求unique_ptr指向的類型和shared_ptr指向的類型能夠互換
shared_ptr<classA> sp2(up); //讓智能指針指向unique_ptr指向的對象,要求unique_ptr指向的類型和shared_ptr指向的類型能夠互換
shared_ptr<classA> sp3(p_A,mydelete) //讓智能指針指向p_A(普通指針)指向的對象,在所有智能指針都釋放以後,不再使用delete釋放內存,而是使用mydelete
shared_ptr<classA> sp4(sp1,mydelete) //讓智能指針指向sp1(智能指針),並使用mydelete代替delete來釋放內存
sp.reset() //把sp從當前指向關係中解放出來,如果sp是最後一個指針,那麼調用delete釋放目標對象
sp.reset(sp1) //把sp重定向到sp1
sp.reset(sp1,mydelete) //把sp中定向到sp1,如果原sp指向的對象已經沒有指針再指向,那麼使用mydelete來釋放原對象
注:如果智能指針指向的不是new出來的動態內存,如果需要釋放資源,那麼一定要提供mydelete來替代delete,因爲這種情況下不會調用delete
#(!)14 ===========================================================================================================================
(!!!)
智能指針的使用:1)使用 普通指針/其他已存在的智能指針/其他已存在的普通指針,對當前創建的智能指針進行初始化。 (創建 指針 指向當前已有內存)
2)使用 make_shared 創建全新的內存區,然後創建一個全新的智能指針指向它。 (創建 內存空間 和 指針)
3)使用 智能指針的 reset 方法來進行 智能指針的 重定向 和 釋放 (重定向 和 銷燬)
#15 ===========================================================================================================================
weak_ptr
weak_ptr 是給 shared_ptr做補充的,把weak_ptr增加到shared_ptr指向的對象上,不會增加計數,單當shared_ptr全部釋放完以後,
weak_ptr指向的內容也將不存在。 weak_ptr的使用必須 經過自己的lock()方法,這個方法會peek對象是否存在,存在返回true,否則
false,用完釋放weak_ptr亦不會導致計數減少。
weak_prt可以理解爲shared_ptr的監視器。僅僅用來peek數據。