只能指針的行為類似常規指針,重要的區別是它負責自動釋放所指向的對象。智能指針定義在memory頭文件中。
1. auto_ptr(C++11已經舍棄)
由new expression獲得的對象,在auto_ptr對象銷毀時,他所管理的對象也會自動被delete掉。
auto_ptr<string> p1(new string("This is a string"));
auto_ptr<string> p2;
p2 = p1;
上述語句中,如果p1和p2是常規指針,則兩個指針將指向同一個string對象。這是不能接受的,因為程序將試圖刪除同一個對象兩次。
要避免這種問題,方法有多種:
- 定義賦值運算符,使之執行深拷貝。這樣兩個指針將指向不同的對象,其中一個對象是另一個對象的副本,缺點是浪費空間,所以智能指針都未采用此方案。
- 建立所有權概念。對於特定的對象,只能有一個指針可擁有,這樣所擁有對象的智能指針的析構函數會刪除該對象。然后讓賦值操作轉讓所有權。這就是用於auto_ptr和unique_ptr的策略。
- 跟蹤引用對象的智能指針數。這稱為引用計數。例如:賦值時,計數加1,指針過期時,計數減1。當減為0時才調用delete。這就是sheared_ptr采用的策略。
為什么要棄用auto_ptr?
#include<memory>
#include<iostream>
using namespace std;
int main()
{
auto_ptr<string> p1(new string("This is a string"));
auto_ptr<string> p2 = p1; //p1將所有權轉讓給p2,此時p1不再引用該字符串從而變成空指針。
cout << *p1 << endl; //報錯,此時p1已經沒有所指向的內存的所有權。
cout << *p2 << endl;
}
- 使用shared_ptr時運行正常,因為shared_ptr采用引用計數,p1和p2都指向同一塊內存,在釋放空間時因為事先要判斷引用計數值的大小因此不會出現多次刪除一個對象的錯誤。
- 使用unique_ptr時編譯出錯,與auto_ptr不同的是,使用unique_ptr時,程序不會等到運行階段崩潰,而在編譯時出現錯誤。
舍棄auto_ptr的原因:避免因潛在的內存問題導致程序崩潰
2. unique_ptr(替換auto_ptr)
unique_ptr比auto_ptr更加安全,因為auto_ptr有拷貝語義,拷貝后原對象變得無效,再次訪問原對象時會導致程序崩潰;unique_ptr禁止了拷貝語義,但提供了移動語義,即可以使用move()進行控制權限的轉移。
unique_ptr<string> p1(new string("This is a string"));
unique_ptr<string> p2(p1); //編譯出錯,已禁止拷貝
unique_ptr<string> p3 = p1; //編譯出錯,已禁止拷貝
unique_ptr<string> p4 = std :: move(p1); //控制權限轉移
auto_ptr<string> p1(new string("This is a string"));
auto_ptr<string> p2(p1); //編譯通過,運行出錯
auto_ptr<string> p3 = p1; //編譯通過,運行出錯
如果unique_ptr是個臨時右值,編譯器允許拷貝語義。
unique_ptr<string> demo(string *s)
{
unique_ptr<string> temp(new string(s));
return temp;
}
unique_ptr<string> ps;
ps = demo("This is a string");
demo()返回一個臨時unique_ptr,然后ps接管了臨時對象所管理的資源,而返回時臨時的unique_ptr被銷毀,也就是說沒有機會使用unique_ptr來訪問無效的數據。相對於auto_ptr任何情況下都允許拷貝語義,這正是unique_ptr更加靈活的地方。
擴展auto_ptr不能完成的功能:
- unique_ptr可放在容器中,彌補了auto_ptr不能作為容器元素的缺點。
- 管理動態數組
- 自定義資源刪除操作。
3. shared_ptr(引用計數型智能指針)
類似vector,智能指針也是模板,當我們創建一個智能指針時,必須提供額外的信息---指針可以指向的類型。
shared_ptr<string> p1;
默認初始化的智能指針中保存着一個空指針。
智能指針的使用方式與普通指針類似。解引用一個智能指針返回它指向的對象。
make_shared函數
最安全的分配和使用動態內存的方法是調用一個名為make_shared的標准庫函數。此函數在動態內存中分配一個對象並初始化它,返回指向此對象的shared_ptr。
shared_ptr<int> p1 = make_shared<int>(42);
shared_ptr<string> p2 = make_shared<string>(10, '9');
auto p3 = make_shared<vector<string>>();
shared_ptr的拷貝和賦值
- 當進行拷貝或賦值操作時,每個shared_ptr都會記錄有多少個其他shared_ptr指向相同的對象。我們可以認為每個shared_ptr都有一個關聯的計數器,通常稱其為引用計數。
- 一旦一個shared_ptr的計數器變為0,它就會自動釋放自己所管理的對象。
shared_ptr自動銷毀所管理的對象
- 它是通過另一個特殊的成員函數---析構函數(destructor)完成銷毀工作的。類似於構造函數,每個類都有一個析構函數。就像構造函數控制初始化一樣,析構函數控制該類型的對象銷毀時做什么操作。
- 析構函數一般用來釋放對象所分配的資源。
- shared_ptr的析構函數會遞減它所指向的對象的引用計數。如果引用計數變為0,shared_ptr的析構函數就會銷毀對象,並釋放它占用的內存。
shared_ptr還會釋放相關聯的內存
- shared_ptr在無用之后仍然保留的一種可能情況是,你將shared_ptr存放在一個容器中,隨后重排了容器,從而不再需要某些元素。在這種情況下,你應該確保使用erase刪除哪些不再需要的shared_ptr元素。
使用動態生存期的資源的類
- 程序不知道自己需要使用多少個對象(容器類)
- 程序不知道所需對象的准確類型
- 程序需要在多個對象間共享數據(如果兩個對象共享底層的數據,當某個對象被銷毀時,我們不能單方面的銷毀底層數據。)
4. weak_ptr(輔助shared_ptr)
- weak_ptr是一種不控制所指向對象生存期的智能指針,它指向由一個shared_ptr管理的對象。將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數。
- 創建一個weak_ptr時,要用一個shared_ptr來初始化它
- 不能使用weak_ptr直接訪問對象,而必須調用lock。此函數檢查weak_ptr指向的對象是否仍存在。如果存在,lock返回一個指向共享對象的shared_ptr.
5.shared_ptr循環引用
#include<iostream>
#include<memory>
using namespace std;
class ListNode{
public:
int m_value;
shared_ptr<ListNode> prev;
shared_ptr<ListNode> next;
//構造函數
ListNode(int value):m_value(value){
cout << "constructor called!" <<endl;
}
//析構函數
~ListNode(){
cout << "destructor called!" <<endl;
}
};
void test(){
shared_ptr<ListNode> sp1 = make_shared<ListNode>(33);
shared_ptr<ListNode> sp2 = make_shared<ListNode>(44);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1 -> next = sp2;
sp2 -> prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
int main(){
test();
return 0;
}
//運行結果:
constructor called!
constructor called!
1
1
2
2
構造的sp1和sp2在出它們的作用域(即test())時,都沒有被析構,從而造成了內存泄漏。
6.weak_ptr是如何解決循環引用的?
#include<iostream>
#include<memory>
using namespace std;
class ListNode{
public:
int m_value;
weak_ptr<ListNode> prev;
weak_ptr<ListNode> next;
//構造函數
ListNode(int value):m_value(value){
cout << "constructor called!" <<endl;
}
//析構函數
~ListNode(){
cout << "destructor called!" <<endl;
}
};
void test(){
shared_ptr<ListNode> sp1 = make_shared<ListNode>(33);
shared_ptr<ListNode> sp2 = make_shared<ListNode>(44);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1 -> next = sp2;
sp2 -> prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
int main(){
test();
return 0;
}
//運行結果:
constructor called!
constructor called!
1
1
1
1
destructor called!
destructor called!
弱引用不修改對象的引用計數;弱引用能檢測對象是否已經被釋放。
只要把循環引用的一方使用弱引用,即可解除循環引用。