我們先來看一些對象的生存期。全局對象在程序啓動時分配,在程序結束時銷燬。局部static對象在第一次使用前分配,在程序結束時銷燬。局部自動對象,在進入其定義所在的程序塊兒時被創建,離開塊時銷燬。即,它們都是由編譯器自動創建與銷燬。
而動態分配的對象的生存期與它們在哪裏創建的無關,只有當顯式地被釋放時,這些對象才銷燬。
在C++中,動態內存的管理是通過一對運算符來完成的:
new:在動態內存爲對象分配空間並返回一個指向該對象的指針,可以選擇對對象進行初始化。
delete:接受一個動態對象的指針,銷燬該對象,並釋放與之關聯的內存。
動態內存的使用很容易出問題,這裏列舉如下幾點:
緩衝區溢出
空懸指針/野指針
重複釋放: 釋放已經被釋放過了的內存
內存泄漏: 忘記釋放內存
不配對的new[]/delete
先列這幾條,等先了解智能指針的概念以後在來看怎麼用智能指針解決這些問題。
爲了更安全(同時也更容易)地使用動態內存,標準庫提供了智能指針類型來管理動態對象。智能指針的行爲類似於常規指針,重要的區別是它負責自動釋放所指向的對象。
我們首先來看一下shared_ptr類,該類型在memory頭文件中。
shared_ptr類
智能指針也是模板。因此在創建一個智能指針時,必須提供它可以指向的類型。
....
shared_ptr<string> p1; //shared_ptr,可以指向string
shared_ptr<list<int>> p2; //shared_ptr,可以指向int的list
....
默認初始化的智能指針保存着一個空指針。類似與普通指針,解引用一個智能指針返回它指向的對象。如果在一個條件判斷中使用智能指針,效果是檢測它是否爲空。
//如果p1指向一個空string,解引用p1,將一個新值給string
if(p1 && p1->empty()){
*p1 = "hi";
}
最安全的分配和使用動態內存的方法是調用標準庫函數make_shared。定義在頭文件memory中。該函數在動態內存中分配一個對象並初始化
,返回指向此對象的shared_ptr。make_shared也是一個模板,使用方法如下:
//指向一個值爲42的int對象的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
//p4指向一個"9999999999"的string對象
shared_ptr<string> p4 = make_shared<string>(10,'9');
//p5指向一個值初始化的int,即值爲0
shared_ptr<int> p5 = make_shared<int>();
make_shared用其參數來構造給定類型的對象。例如,調用make_shared時傳遞的參數必須與string的某個構造函數相匹配。通常用auto定義一個對象來保存make_shared的結果:
//p6指向一個動態分配的vector<string>
auto p6 = make_shared<vector<string>>();
shared_ptr的拷貝和賦值
當進行拷貝或賦值操作時,每個shared_ptr都會記錄有多少個其他的shared_ptr指向相同的對象。
auto p = make_shared<int>(42); //p指向的對象只有p一個引用者
auto q(p); //p和q指向相同的對象,此對象有兩個引用者。
可以認爲每個shared_ptr都有一個關聯的計數器(引用計數)。無論何時,拷貝一個shared_ptr,計數器都會遞增。例如:
用一個shared_ptr初始化另外一個shared_ptr
將一個shared_ptr作爲一個參數傳遞給一個函數
當一個shared_ptr作爲一個函數的返回值
相對的,給一個shared_ptr賦予一個新值或者shared_ptr被銷燬時,計數器就會遞減。
一旦一個shared_ptr的計數器變爲0,它就會自動釋放自己所管理的對象。
auto r = make_shared<int>(42);
r = q; //q指向的對象的引用計數遞增。
//r原來指向的對象的引用計數遞減。
//r原來指向的對象沒有了引用者,會自動釋放。
當指向一個對象的最後一個shared_ptr被銷燬時,shared_ptr類會自動銷燬此對象,它是通過析構函數來完成銷燬動作的。shared_ptr的析構函數會遞減它所指向的對象的引用計數,如果引用計數邊爲0,shared_ptr的析構函數就會銷燬對象,並釋放它佔用的內存。
對於一塊內存,shared_ptr類保證只要有任何shared_ptr對象引用它,它就不會被釋放掉。
但是,如果你忘記了銷燬程序不需要的shared_ptr,程序仍會正確執行,但會浪費內存。一種可能的情況是,將shared_ptr放在一個容器裏,當不需要某些元素時,應該確保用erase刪除那些不需要的shared_ptr元素。
程序使用動態生存期的資源的類 出於以下三種原因之一:
程序不知道自己需要使用多少對象(空間)例如,容器類
程序不知道所需對象的準確類型
程序需要在多個對象間共享數據
下面是一個需要在多個對象間共享數據的例子:
我們定義一個strblob類,保存一組元素。與容器不同,我們希望strblob對象的不同拷貝之間共享相同的元素。我們可以用一個vector來保存元素,但是,不能在一個strblob對象內直接保存vector,因爲一個對象的數據成員在對象銷燬時也會被銷燬。爲了實現strblob對象共享相同的底層數據,我們可以將vector保存在動態內存中,然後爲每個strblob設置一個shared_ptr來管理動態內存分配的vector。這個shared_ptr的成員將記錄有多少個shared_ptr共享相同的vector,並在vector的最後一個使用者被銷燬時釋放vector。
#ifndef _STRBLOB_H
#define _STRBLOB_H
#include<string>
#include<memory>
#include<list>
#include<vector>
using namespace std;
class strblob
{
public:
strblob();
strblob(std::initializer_list<std::string> s);
void print(){
for(auto v : *data_){
std::cout << v << std::endl;
}
}
private:
std::shared_ptr<std::vector<std::string>> data_;
};
strblob::strblob():data_( make_shared<vector<string>>() ){}
strblob::strblob(initializer_list<string> s):data_(make_shared<vector<string>>(s)){}
#endif
下面是一個測試用例
#include"strblob.h"
int main(int argc,char *argv[])
{
std::initializer_list<std::string> s = {"hello"};
strblob A; //使用不帶參數的構造函數
strblob B(s); //使用帶參數的構造函數
A.print();
B.print();
std::cout << std::endl;
//B對象的shred_ptr指向的對象的引用計數遞增,A的遞減並且釋放A的shared_ptr指向的對象
A = B; //使用默認賦值
A.print();
B.print();
return 0;
}
其他shared_ptr操作:
我們可以用reset來將一個新的指針賦予一個shared_ptr
p.reset(); //若p是唯一指向對象的shared_ptr,reset會釋放此對象。
p.reset(q); //若傳遞了可選參數內置指針q,會令p指向q,否則會將p置爲空。
p.reset(q,d); //如果還傳遞了參數d,將會調用d而不是delete來釋放q
weak_ptr
weak_ptr是一種不控制所指向對象生存期的智能指針,它指向由一個shared_ptr管理的對象。將一個weak_ptr綁定到shared_ptr不會改變shared_ptr的引用計數。一旦最後一個指向對象的shared_ptr被銷燬,對象就會被釋放。即使有weak_ptr指向對象,對象也還是會被釋放,因此weak_ptr的名字抓住了這種智能指針“弱”共享對象的特點。
當我們創建一個weak_ptr時,要用一個shared_ptr來初始化它:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); //wp弱共享p; p的引用計數未改變。
由於waek_ptr指向的對象可能不存在,因此我們不能直接使用weak_ptr來訪問對象,而必須調用lock()。此函數檢查weak_ptr指向的對象是否仍存在,如果存在,lock返回一個指向共享對象的shared_ptr。與任何其他shared_ptr類似,只要此shared_ptr存在,它所指向的底層對象也就會一直存在。
if(auto np = wp.lock()){
//進入if條件證明wp指向的對象存在,在這裏np與p共享對象
}