C#、Java、python和go等語言中都有垃圾自動回收機(jī)制,在對象失去引用的時(shí)候自動回收,而且基本上沒有指針的概念,而C++語言不一樣,C++充分信任程序員,讓程序員自己去分配和管理堆內(nèi)存,如果管理的不好,就會很容易的發(fā)生內(nèi)存泄漏問題,而C++11增加了智能指針(Smart Pointer)。主要分為shared_ptr、unique_ptr和weak_ptr三種,使用時(shí)需要引用頭文件<memory>。c++98中還有auto_ptr,基本被淘汰了,不推薦使用。而c++11中shared_ptr和weak_ptr都是參考的boost庫中實(shí)現(xiàn)的。
本文有很多代碼片段,如果不喜歡簡書的代碼風(fēng)格,請移步我的SegmentFault博客。
原始指針
c語言中最常使用的是malloc()函數(shù)分配內(nèi)存,free()函數(shù)釋放內(nèi)存,而c++中對應(yīng)的是new、delete關(guān)鍵字。malloc()只是分配了內(nèi)存,而new則更進(jìn)一步,不僅分配了內(nèi)存,還調(diào)用了構(gòu)造函數(shù)進(jìn)行初始化。使用示例:
int main()
{
// malloc返回值是 void*
int* argC = (int*)malloc(sizeof(int));
free(argC);
char*c = (char*)malloc(100);
free(c);
char *age = new int(25); //做了兩件事情 1.分配內(nèi)存 2.初始化
int* height = new int(160);
delete height;
delete age;
char* arr = new int[100];
delete[] arr;
/*delete數(shù)組需要使用delete[],事實(shí)上,c++原始支持的數(shù)據(jù)結(jié)構(gòu)組成的
數(shù)組不需要[]也可以,但 自定義的數(shù)據(jù)類型組成的數(shù)組必須使用delete[]*/
}
new和delete必須成對出現(xiàn),有時(shí)候是不小心忘記了delete,有時(shí)候則是很難判斷在這個地方自己是不是該delete,這個和資源的生命周期有關(guān),這個資源是屬于我這個類管理的還是由另外一個類管理的,如果是我管理的,就由我來delete,由別人管理的就由別人delete,我就算析構(gòu)了也不影響該資源的生命周期。例如:
// 情況1: 需要自己delete
const char* getName() {
char *valueGroup = new char[1000];
// do something
return valueGroup;
}
// 情況2: 不需要自己delete
const char* getName2() {
static char valueGroup[1000];
// do something
return valueGroup;
}
只通過函數(shù)簽名來看,這兩個函數(shù)沒有什么區(qū)別,但是由于實(shí)現(xiàn)的不同,有時(shí)候需要自己管理內(nèi)存,有時(shí)候不需要,這個時(shí)候就需要看文檔說明了。這就是使用一個"裸"指針不好的地方。
一點(diǎn)改進(jìn)是,如果需要自己管理內(nèi)存的話,最好顯示的將自己的資源傳遞進(jìn)去,這樣的話,就能知道是該資源確實(shí)應(yīng)該由自己來管理。
char *getName(char* v, size_t bufferSize) {
//do something
return v;
}
上面還是小問題,自己小心一點(diǎn),再仔細(xì)看看文檔,還是有機(jī)會避免這些情況的。但是在c++引入異常的概念之后,程序的控制流就發(fā)生了根本性的改變,在寫了delete的時(shí)候還是有可能發(fā)生內(nèi)存泄漏。如下例:
void badThing(){
throw 1;// 拋出一個異常
}
void test() {
char* a = new char[1000];
badThing();
// do something
delete[] a;
}
int main() {
try {
test();
}
catch (int i){
cout << "error happened " << i << endl;
}
}
上面的new和delete是成對出現(xiàn)的,但是程序在中間的時(shí)候拋出了異常,由于沒有立即捕獲,程序從這里退出了,并沒有執(zhí)行到delete,內(nèi)存泄漏還是發(fā)生了。
C++中的構(gòu)造函數(shù)和析構(gòu)函數(shù)十分強(qiáng)大,可以使用構(gòu)造和析構(gòu)解決這種問題,比如:
class SafeIntPointer {
public:
explicit SafeIntPointer(int v) : m_value(new int(v)) { }
~SafeIntPointer() {
delete m_value;
cout << "~SafeIntPointer" << endl;
}
int get() { return *m_value; }
private:
int* m_value;
};
void badThing(){
throw 1;// 拋出一個異常
}
void test() {
SafeIntPointer a(5);
badThing();
}
int main() {
try {
test();
}
catch (int i){
cout << "error happened " << i << endl;
}
}
// 結(jié)果
// ~SafeIntPointer
// error happened 1
可以看到,就算發(fā)生了異常,也能夠保證析構(gòu)函數(shù)成功執(zhí)行!這里的例子是這個資源只有一個人使用,我不用了就將它釋放掉。但還有種情況,一份資源被很多人共同使用,要等到所有人都不再使用的時(shí)候才能釋放掉,對于這種問題,就需要對上面的SafeIntPointer增加一個引用計(jì)數(shù),如下:
class SafeIntPointer {
public:
explicit SafeIntPointer(int v) : m_value(new int(v)), m_used(new int(1)) { }
~SafeIntPointer() {
cout << "~SafeIntPointer" << endl;
(*m_used) --; //引用計(jì)數(shù)減1
if(*m_used <= 0){
delete m_used;
delete m_value;
cout << "real delete resources" << endl;
}
}
SafeIntPointer(const SafeIntPointer& other) {
m_used = other.m_used;
m_value = other.m_value;
(*m_used)++; //引用計(jì)數(shù)加1
}
SafeIntPointer& operator= (const SafeIntPointer& other) {
if (this == &other) // 避免自我賦值!!
return *this;
m_used = other.m_used;
m_value = other.m_value;
(*m_used)++; //引用計(jì)數(shù)加1
return *this;
}
int get() { return *m_value; }
int getRefCount() {
return *m_used;
}
private:
int* m_used;
int* m_value;
};
int main() {
SafeIntPointer a(5);
cout << "ref count = " << a.getRefCount() << endl;
SafeIntPointer b = a;
cout << "ref count = " << a.getRefCount() << endl;
SafeIntPointer c = b;
cout << "ref count = " << a.getRefCount() << endl;
}
/*
ref count = 1
ref count = 2
ref count = 3
~SafeIntPointer
~SafeIntPointer
~SafeIntPointer
real delete resources
*/
可以看到每一次賦值,引用計(jì)數(shù)都加一,最后每次析構(gòu)一次后引用計(jì)數(shù)減一,知道引用計(jì)數(shù)為0,才真正釋放資源。要寫出一個正確的管理資源的包裝類還是蠻難的,比如上面那個上面例子就不是線程安全的,只能屬于一個玩具,在實(shí)際工程中簡直沒法用。而到了C++11,終于提供了一個共享的智能指針解決這個問題。
shared_ptr共享的智能指針
shared_ptr的基本使用
shared_ptr的基本使用很簡單,看幾個例子就明白了:
#include <iostream>
#include <memory>
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye" << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 取個別名 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;
void print(Object* obj) {
std::cout << "in print(Object* obj) : " << std::endl;
// std::cout << "ref count is " << obj.use_count() << std::endl;
}
void print(Object obj) {
std::cout << "in print(Object obj) : " << std::endl;
// std::cout << "ref count is " << obj.use_count() << std::endl;
}
void print(ObjectPtr obj) {
std::cout << "in print(ObjectPtr obj) : ";
std::cout << "ref count is " << obj.use_count() << std::endl;
}
void printRef(const ObjectPtr& obj) {
std::cout << "in print(const ObjectPtr& obj) : ";
std::cout << "ref count is " << obj.use_count() << std::endl;
}
int main() {
// 創(chuàng)建一個智能指針,管理的資源就是 new出來的Object(1)
//ObjectPtr obj=new Object(1); //不能將一個原始指針直接賦值給一個智能指針
ObjectPtr obj(new Object(1)); // 正確
std::cout << "ref count is " << obj.use_count() << std::endl; // 1
ObjectPtr obj2(obj);
std::cout << "ref count is " << obj.use_count() << std::endl; // 2
std::cout << "ref count is " << obj2.use_count() << std::endl; // 2 obj和obj2管理的資源是一樣的
ObjectPtr obj3 = obj2;
std::cout << "ref count is " << obj.use_count() << std::endl; // 3
obj2.reset(); // obj2不再管理之前的資源,資源的引用計(jì)數(shù)會減1
// 或者可以寫成 obj2 = nullptr;
std::cout << "ref count is " << obj.use_count() << std::endl; // 2
ObjectPtr obj4; //obj4 開始沒有管理資源
// 將管理的資源 相互交換
// 交換后 obj3沒有再管理資源, obj4管理obj3之前管理的資源
// 或者寫成 std::swap(obj3, obj4);
obj3.swap(obj4);
std::cout << "ref count is " << obj.use_count() << std::endl; // 還是 2
// 還可以從智能指針中獲取原始指針
auto p = obj.get(); // auto = Object*
// 需要判斷這個obj是否確實(shí)管理著資源, 可能為 nullptr
if( p ) {
std::cout << "p->id() is " << p->id() << std::endl;
std::cout << "(*p).id() is " << (*p).id() << std::endl;
}
// 智能指針也可以像普通指針一樣使用
// 重載了 operator bool
if( obj ) {
std::cout << "obj->id() is " << obj->id() << std::endl; // 重載了operator ->
std::cout << "(*obj).id() is " << (*obj).id() << std::endl;// 重載了operator *
}
// obj.use_count()可以判斷當(dāng)前有多少智能指針在管理資源
// 如果判斷是不是只有一個人在管理這個資源, 用unique()函數(shù)更加高效
// unique() 等價(jià)于 obj.use_count() == 1
obj4 = nullptr; // obj4不在管理,這個時(shí)候的引用計(jì)數(shù)變成了 1
std::cout << "ref count is " << obj.use_count() << std::endl; // 1
if( obj.unique() )
std::cout << "only one hold ptr "<< std::endl;
else
std::cout << "not noly one hold ptr" << std::endl; //其實(shí)也有可能沒有人再管理
// 將智能指針當(dāng)作參數(shù)傳遞給函數(shù)時(shí)
// 如果是值傳遞, 智能指針發(fā)生一次拷貝,
// 在函數(shù)內(nèi)部時(shí)智能指針的引用計(jì)數(shù)會 + 1
// 離開函數(shù)作用域時(shí), 智能指針會析構(gòu), 引用計(jì)數(shù)會 - 1
print(obj);
// 如果傳遞的是引用, 對引用計(jì)數(shù)沒影響 而且工作量比較小(沒有拷貝)
// 推薦使用引用方式傳遞, 傳值的方式也有用處,比如多線程時(shí)
printRef(obj);
// 還可以不傳遞智能指針, 傳遞原生類型
print(*obj); //傳Object類型的時(shí)候,離開函數(shù)的時(shí)候參數(shù)obj會發(fā)生一次析構(gòu)
print(obj.get());
}
/*
init obj
ref count is 1
ref count is 2
ref count is 2
ref count is 3
ref count is 2
ref count is 2
p->id() is 1
(*p).id() is 1
obj->id() is 1
(*obj).id() is 1
ref count is 1
only one hold ptr
in print(ObjectPtr obj) : ref count is 2
in print(const ObjectPtr& obj) : ref count is 1
in print(Object obj) :
bye bye1 //這個是在調(diào)用print(Object obj)時(shí),局部變量析構(gòu)時(shí)打印的
in print(Object* obj) :
bye bye1 //這個是在資源在沒有人引用的時(shí)候,執(zhí)行析構(gòu)函數(shù)產(chǎn)生的
*/
給shared_ptr指定刪除器
大部分用法都基本上在上面的例子中體現(xiàn)出來了,當(dāng)沒有人引用這個資源的時(shí)候,智能指針的默認(rèn)行為是調(diào)用 delete銷毀這個資源,而我們也可以人為指定這個步驟,因?yàn)橛行┵Y源不一定是new出來的,所以不應(yīng)該使用默認(rèn)的delete行為,還有一個情況是,在用智能指針管理動態(tài)數(shù)組的時(shí)候,需要自己指定刪除器函數(shù)。
#include <iostream>
#include <memory>
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye" << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;
void deleterOfObject(Object* obj) {
if ( obj ) {
std:: cout << "delete obj " << obj->id() << std::endl;
delete obj;
}
}
void useDeleter() {
//指定刪除動作 使用外面定義的函數(shù)
ObjectPtr obj(new Object(2), deleterOfObject);
ObjectPtr obj2 = obj;
// obj 和 obj2 會在離開這個函數(shù)的時(shí)候析構(gòu),于是,就調(diào)用了 deleterOfObject
//管理數(shù)組 使用匿名函數(shù)當(dāng)作刪除函數(shù)
std::shared_ptr<int> p(new int[10], [](int* p){
std::cout << "delete[] p" << std::endl;
delete[] p; //需要使用delete[]
});
// vector<> 沒必要使用智能指針, 不用new 和 delete... 內(nèi)部已經(jīng)管理了
}
int main() {
useDeleter();
}
/*
init obj
delete[] p //注意析構(gòu)和構(gòu)造的順序是相反的
delete obj 2
bye bye2
*/
shared_ptr主要就是利用變量出了作用域之后析構(gòu)函數(shù)一定能被調(diào)用到,哪怕是出現(xiàn)了異常。
不要用一個原始的指針初始化多個shared_ptr
例如下面的例子:
#include <iostream>
#include <memory>
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye " << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;
int main() {
Object *obj = new Object(2);
ObjectPtr p1(obj);
ObjectPtr p2(obj);
std::cout << p1.use_count() << " " << p2.use_count() << std::endl;
std::cout << "finished" << std::endl;
}
/*
init obj
1 1
finished
bye bye 2
bye bye 203200 //m_id成為了隨機(jī)數(shù)
*/
可以發(fā)現(xiàn),雖然是用的同一個指針初始化了兩個shared_ptr,但是這兩個shared_ptr卻沒有關(guān)聯(lián),它們的引用計(jì)數(shù)都是1,然后問題就發(fā)生了,p2先析構(gòu),于是引用計(jì)數(shù)變?yōu)榱?code>0,就開始刪除它管理的資源obj,于是obj就被析構(gòu)了,這是還算正常,接著析構(gòu)p1,引用計(jì)算也變成了0,它也開始刪除自己管理的資源obj,相當(dāng)于多次delete了同一個對象,m_id成為了隨機(jī)數(shù),這還算好的情況,如果Object內(nèi)部還有指針,或者obj的地址被其他變量占據(jù)了,delete掉這塊內(nèi)存就會發(fā)生嚴(yán)重的錯誤!而且不好發(fā)現(xiàn)原因。
將this指針正確的傳遞給shared_ptr
其實(shí)就是由于上面的原因,我們不可能傳遞this指針給shared_ptr,因?yàn)橛猛粋€指針初始化兩個shared_ptr,它們之間并沒有關(guān)聯(lián),如下面的例子:
#include <iostream>
#include <memory>
class Y
{
public:
std::shared_ptr<Y> f(){
return std::shared_ptr<Y>(this);
}
};
int main()
{
std::shared_ptr<Y> p1(new Y());
std::shared_ptr<Y> p2 = p1->f(); // p2是由this構(gòu)造共享智能指針
std::cout << p1.use_count() << " " << p2.use_count() << std::endl; // 1 1
}
從上面的例子可以看出,返回由this構(gòu)造的shared_ptr并沒有用,返回還可能造成嚴(yán)重錯誤(由于可能多次delete)!解決辦法是繼承std::enable_shared_from_this<Y>,然后使用shared_from_this()構(gòu)造shared_ptr。
#include <iostream>
#include <memory>
class Y : public std::enable_shared_from_this<Y>
{
public:
std::shared_ptr<Y> f(){
return shared_from_this();
}
};
int main()
{
std::shared_ptr<Y> p1(new Y());
std::shared_ptr<Y> p2 = p1->f(); // p2是由p1的thiss構(gòu)造共享智能指針
std::cout << p1.use_count() << " " << p2.use_count() << std::endl; // 2 2
std::shared_ptr<Y> p3(new Y());
std::shared_ptr<Y> p4 = p3->f(); // p4是由p3的this構(gòu)造的構(gòu)造共享智能指針
std::cout << p1.use_count() << " " << p2.use_count() << " "
<< p3.use_count() << " " << p4.use_count() << std::endl; // 2 2 2 2
}
可以發(fā)現(xiàn)引用計(jì)數(shù)確實(shí)增加了。并且由p1得到的shared_from_this()增加的就是p1的引用計(jì)數(shù),p3得到的shared_from_this()增加的就是p3的引用計(jì)數(shù),這和this的含義是一樣的。所以我們在類內(nèi)部需要傳遞this指針給shared_ptr時(shí),需要繼承自std::enable_shared_from_this<T>,并且使用shared_from_this()替代this。而shared_from_this()就是借助了weak_ptr。原理在后面再講。
shared_ptr的正確構(gòu)造方式
其實(shí)上面使用的智能指針構(gòu)造方式有一點(diǎn)點(diǎn)問題,ObjectPtr obj(new Object(1));這一個語句其實(shí)調(diào)用了兩次new,一次是new Object(1),另一次是構(gòu)造內(nèi)部的引用計(jì)數(shù)變量的時(shí)候,那有沒有辦法只掉用一次new呢,答案就是使用make_shared<T>()模板函數(shù),它將資源和引用計(jì)數(shù)變量一起new出來,例如:
#include <iostream>
#include <memory>
#include <cassert>
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye " << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;
int main() {
// 和 ObjectPtr obj(new Object(2)); 一樣
// 但是只調(diào)用了一次new
ObjectPtr obj = std::make_shared<Object>(2);
}
然而,這個函數(shù)也有失效的時(shí)候,如果管理的資源對象的構(gòu)造函數(shù)是私有的他就沒有辦法了。
weak_ptr弱引用的智能指針
循環(huán)引用問題的引出
在有些情況下,shared_ptr也會遇見很尷尬、不能處理的情況,那就是循環(huán)引用,看下面的例子:
#include <iostream>
#include <memory>
class Parent; //Parent類的前置聲明
typedef std::shared_ptr<Parent> ParentPtr;
class Child {
public:
ParentPtr father;
~Child() {
std::cout << "bye child" << std::endl;
}
};
typedef std::shared_ptr<Child> ChildPtr;
class Parent {
public:
ChildPtr son;
~Parent() {
std::cout << "bye parent" << std::endl;
}
};
void testParentAndChild() {
ParentPtr p(new Parent()); // 1 資源A
ChildPtr c(new Child()); // 2 資源B
p->son = c; // 3 c.use_count() == 2 and p.use_count() == 1
c->father = p; // 4 c.use_count() == 2 and p.use_count() == 2
}
int main() {
testParentAndChild();
std::cout << "finished" << std::endl;
}
/*
// 沒有調(diào)用Parent 和 Child 的析構(gòu)函數(shù)
finished
*/
很驚訝的發(fā)現(xiàn),用了shared_ptr管理資源,資源最后還是沒有釋放!內(nèi)存泄漏還是發(fā)生了。
分析:
- 執(zhí)行編號
1的語句時(shí),構(gòu)造了一個共享智能指針p,稱呼它管理的資源叫做資源A(new Parent()產(chǎn)生的對象)吧, 語句2構(gòu)造了一個共享智能指針c,管理資源B(new Child()產(chǎn)生的對象),此時(shí)資源A和B的引用計(jì)數(shù)都是1,因?yàn)橹挥?code>1個智能指針管理它們,執(zhí)行到了語句3的時(shí)候,是一個智能指針的賦值操作,資源B的引用計(jì)數(shù)變?yōu)榱?code>2,同理,執(zhí)行完語句4,資源A的引用計(jì)數(shù)也變成了2。 - 出了函數(shù)作用域時(shí),由于析構(gòu)和構(gòu)造的順序是相反的,會先析構(gòu)共享智能指針
c,資源B的引用計(jì)數(shù)就變成了1;接下來繼續(xù)析構(gòu)共享智能指針p,資源A的引用計(jì)數(shù)也變成了1。由于資源A和B的引用計(jì)數(shù)都不為1,說明還有共享智能指針在使用著它們,所以不會調(diào)用資源的析構(gòu)函數(shù)! - 這種情況就是個死循環(huán),如果資源
A的引用計(jì)數(shù)想變成0,則必須資源B先析構(gòu)掉(從而析構(gòu)掉內(nèi)部管理資源A的共享智能指針),資源B的引用計(jì)數(shù)想變?yōu)?code>0,又得依賴資源A的析構(gòu),這樣就陷入了一個死循環(huán)。
要想解決這個問題,只能引入新的智能指針weak_ptr,顧名思義,弱引用,也就是不增加引用計(jì)數(shù),它不管理shared_ptr內(nèi)部管理的指針,他只是起一個監(jiān)視的作用。它監(jiān)視的不是shared_ptr本身,而是shared_ptr管理的資源?。?!weak_ptr沒有重載操作符*和->,它不能直接操作資源,但是它可以獲取所監(jiān)視的shared_ptr(如果資源還沒有被析構(gòu)的話)。
weak_ptr的基本用法
weak_ptr使用示例:
#include <iostream>
#include <memory>
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye" << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 取個別名 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;
void sharedPtrWithWeakPtr() {
ObjectPtr obj(new Object(1));
typedef std::weak_ptr<Object> WeakObjectPtr;
WeakObjectPtr weakObj(obj); //使用共享指針 初始化 弱引用指針
//weakObj 僅僅是一個監(jiān)聽者,不會增加引用計(jì)數(shù)
std::cout << "obj use count is " << obj.use_count() << std::endl; // 1
{
// lock() 方法返回一個 它對應(yīng)的共享指針
// 下面這句話的結(jié)果是 2, 而不是1,
// 說明weakObj.lock() 內(nèi)部也得到了一個新的共享指針,所以引用計(jì)數(shù)+1
// 在執(zhí)行完這句話后就析構(gòu)掉了,引用計(jì)數(shù)-1
std::cout << "weakObj.lock().use_count() is " << weakObj.lock().use_count() << std::endl; // 2
// 由于發(fā)生了一次 賦值 ,所以 引用次次數(shù) +1
// auto === ObjectPtr
auto p = weakObj.lock(); //如果weakObj監(jiān)視的資源存在, p就存在
std::cout << "obj use count is " << obj.use_count() << std::endl; // 2
if ( p ) {
// do what you want to do
} else {
}
}
// 共享指針不再管理任何資源的時(shí)候,weakObj的行為
// 注意:如果在obj.reset前,還存在共享指針管理它的資源
// 如 :ObjectPtr obj1(obj); weakObj.lock();還是有效的
obj.reset();
{
auto p = weakObj.lock();
if( p ) {
//不應(yīng)該到這里來
std::cout << "weakObj is not null 1" << std::endl;
} else {
std::cout << "weakObj is null 1" << std::endl;
}
}
// 共享指針管理其他資源的時(shí)候,weakObj的行為
// 注意:weak_ptr.lock()
// 只有在 存在某一個shared_ptr管理的資源和該weak_ptr一樣 的時(shí)候才有效果!
obj.reset(new Object(2));
{
auto p = weakObj.lock();
if( p ) {
//不應(yīng)該到這里來
std::cout << "weakObj is not null 2" << std::endl;
} else {
std::cout << "weakObj is null 2" << std::endl;
}
}
weakObj = obj; // 重新監(jiān)視 obj
// 用weakObj 判斷管理的資源是否過期
if(weakObj.expired()) {
} else {
}
}
int main() {
sharedPtrWithWeakPtr();
std::cout << "finished" << std::endl;
}
/*
init obj
obj use count is 1
weakObj.lock().use_count() is 2
obj use count is 2
bye bye1
weakObj is null 1
init obj
weakObj is null 2
bye bye2
finished
*/
由上面的例子可以看出,weak_ptr和初始化它的share_ptr沒有關(guān)系,而是和share_ptr管理的資源有關(guān)系。假如WeakObjectPtr weakObj(obj);,如果obj.reset(),weakObj.lock()的返回值就是空,如果obj.reset(new Object(2));,替換了管理對象,則一起的資源就被析構(gòu)了,weakObj.lock()的返回值同樣為空,同樣可以推斷,如果除了obj以外還有其他共享智能指針一起管理資源,也就是說obj.reset()的時(shí)候資源不會被析構(gòu),weakObj.lock();的返回值就不會為空了。不明白的話自己寫個簡單的測試用例就知道了,如:
void sharedPtrWithWeakPtr() {
ObjectPtr obj(new Object(1));
typedef std::weak_ptr<Object> WeakObjectPtr;
WeakObjectPtr weakObj(obj); //使用共享指針 初始化 弱引用指針
ObjectPtr obj1 = obj; //注釋掉這句話打印的就是error, 加上這句話打印的就是ok
obj.reset();
auto p = weakObj.lock();
if( p ) {
std::cout << "ok" << std::endl;
} else {
std::cout << "error" << std::endl;
}
}
weak_ptr解決循環(huán)引用
用weak_ptr可以解決上面的循環(huán)引用問題,將Child內(nèi)部的parent指針換成weak_ptr管理:
#include <iostream>
#include <memory>
class Parent; //Parent類的前置聲明
typedef std::shared_ptr<Parent> ParentPtr;
typedef std::weak_ptr<Parent> WeakParentPtr;
class Child {
public:
WeakParentPtr father;
~Child() {
std::cout << "bye child" << std::endl;
}
};
typedef std::shared_ptr<Child> ChildPtr;
//typedef std::weak_ptr<Child> WeakChildPtr;
class Parent {
public:
//WeakChildPtr son;
ChildPtr son;
~Parent() {
std::cout << "bye parent" << std::endl;
}
};
void testParentAndChild() {
ParentPtr p(new Parent()); // 1 資源A
ChildPtr c(new Child()); // 2 資源B
p->son = c; // 3 c.use_count() == 2 and p.use_count() == 1
c->father = p; // 4 c.use_count() == 2 and p.use_count() == 1
}
int main() {
testParentAndChild();
std::cout << "finished" << std::endl;
}
/*
bye parent //成功調(diào)用析構(gòu)函數(shù)
bye child
finished
*/
修改為弱引用后,成功的釋放了資源,只要將任意一個shared_ptr換成weak_ptr,就可以解決問題。當(dāng)然,也可以兩個都換成weak_ptr,至于這三種方案誰更好,就暫時(shí)不清楚了。
shared_from_this()實(shí)現(xiàn)原理
std::enable_shared_from_this<T>模板類中有一個weak_ptr,這個weak_ptr用來觀測this智能指針,調(diào)用shared_from_this()函數(shù)的時(shí)候,會在內(nèi)部調(diào)用weak_ptr的lock()方法,將所觀測的shared_ptr返回。這個設(shè)計(jì)要依賴于當(dāng)前對象已經(jīng)有了一個相應(yīng)的控制塊。為此,必須已經(jīng)存在一個指向當(dāng)前對象的shared_ptr(比如在調(diào)用過shared_from_this()成員函數(shù)之外已經(jīng)有了一個)。假如沒有這樣shared_ptr存在,那么shared_from_this()會拋異常。 那么這個weak_ptr在什么時(shí)候賦值的呢?答案就是在外部第一次構(gòu)造shared_ptr的時(shí)候(如之前的std::shared_ptr<Y> p1(new Y());),對std::enable_shared_from_this<T>進(jìn)行了賦值(具體實(shí)現(xiàn)有點(diǎn)復(fù)雜,還不太懂。。),這也就為什么在調(diào)用shared_from_this()時(shí),必須存在一個指向當(dāng)前對象的shared_ptr的原因了。由于這個原因,不要在構(gòu)造函數(shù)中調(diào)用shared_from_this(),如:
#include <iostream>
#include <memory>
class Y : public std::enable_shared_from_this<Y>
{
public:
Y() {
std::shared_ptr<Y> p = shared_from_this();
}
std::shared_ptr<Y> f(){
return shared_from_this();
}
};
int main()
{
std::shared_ptr<Y> p1(new Y());
}
// 會拋出異常!
/*
terminate called after throwing an instance of 'std::bad_weak_ptr'
what(): bad_weak_ptr
*/
unique_ptr獨(dú)占的智能指針
unique_ptr相對于其他兩個智能指針更加簡單,它和shared_ptr使用差不多,但是功能更為單一,它是一個獨(dú)占型的智能指針,不允許其他的智能指針共享其內(nèi)部的指針,更像原生的指針(但更為安全,能夠自己釋放內(nèi)存)。不允許賦值和拷貝操作,只能夠移動。
#include <iostream>
#include <memory>
#include <cassert>
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye " << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;
typedef std::unique_ptr<Object> UniqueObjectPtr;
// 只能傳遞引用 不能傳值
void print(const UniqueObjectPtr& obj) {
std::cout << obj->id() << std::endl;
}
int main() {
UniqueObjectPtr obj(new Object(1));
// UniqueObjectPtr obj1 = obj; // 編譯錯誤,不允許賦值
// 獲取原生指針
auto p = obj.get();
if( p ) {
} else {
}
// better 重載了 operator bool
if(obj) {
} else {
}
// 重載了 operator -> 和 operator *
std::cout << obj->id() << " " << (*obj).id() << std::endl;
print(obj);
// 釋放管理的指針,由其他東西處理
p = obj.release();
// delete p; 自己負(fù)責(zé)處理
obj.reset(p); //析構(gòu)之前負(fù)責(zé)管理的對象,重新管理 p指針
obj.reset(); // 析構(gòu)之前負(fù)責(zé)管理的對象, 不再管理任何資源
// 允許 移動操作
UniqueObjectPtr obj1(new Object(1));
// obj1已經(jīng)部管理任何資源 obj2開始管理obj1之前的資源
UniqueObjectPtr obj2 = std::move(obj1);
assert(obj1 == nullptr);
std::cout << obj2->id() << std::endl;
// 將unique_ptr管理的內(nèi)容給 shared_ptr
ObjectPtr obj3(std::move(obj2));
assert(obj2 == nullptr);
}
/*
init obj
1 1
1
bye bye 1
init obj
1
bye bye 1
*/
unique_ptr管理數(shù)組資源不需要指定刪除器:
std::shared_ptr<int> p(new int[10], [](int* p){
std::cout << "delete[] p" << std::endl;
delete[] p; //需要使用delete[]
});
std::unique_ptr<int> p2(new int[10]); //不需要指定刪除器
性能與安全的權(quán)衡
使用智能指針雖然能夠解決內(nèi)存泄漏問題,但是也付出了一定的代價(jià)。以shared_ptr舉例:
-
shared_ptr的大小是原始指針的兩倍,因?yàn)樗膬?nèi)部有一個原始指針指向資源,同時(shí)有個指針指向引用計(jì)數(shù)。 - 引用計(jì)數(shù)的內(nèi)存必須動態(tài)分配。雖然一點(diǎn)可以使用
make_shared()來避免,但也存在一些情況下不能夠使用make_shared()。 - 增加和減小引用計(jì)數(shù)必須是原子操作,因?yàn)榭赡軙凶x寫操作在不同的線程中同時(shí)發(fā)生。比如在一個線程里有一個指向一塊資源的
shared_ptr可能調(diào)用了析構(gòu)(因此所指向的資源的引用計(jì)數(shù)減一),同時(shí),在另一線程里,指向相同對象的一個shared_ptr可能執(zhí)行了拷貝操作(因此,引用計(jì)數(shù)加一)。原子操作一般會比非原子操作慢。但是為了線程安全,又不得不這么做,這就給單線程使用環(huán)境帶來了不必要的困擾。
我覺得還是分場合吧,看應(yīng)用場景來進(jìn)行權(quán)衡,我也沒啥經(jīng)驗(yàn),但我感覺安全更重要,現(xiàn)在硬件已經(jīng)足夠快了,其他例如java這種支持垃圾回收的語言不還是用的很好嗎。
總結(jié)
- 智能指針主要是使用構(gòu)造和析構(gòu)來管理資源的。
-
shared_ptr很好用也很難用,有兩種構(gòu)造方式,使用引用計(jì)數(shù)實(shí)現(xiàn)多人同時(shí)管理一份資源。使用this的時(shí)候要格外注意。 -
weak_ptr可以解決shared_ptr的循環(huán)引用問題。 -
unique_ptr最像裸指針,但更為安全,保證資源的釋放,不能復(fù)制只能移動。 - 智能指針帶來了性能問題,在不同場合可以選擇不同的解決方案。優(yōu)先使用類的實(shí)例(如果內(nèi)存足夠),其次
unique_ptr,最后才是shared_ptr。