程式使用三種不同的記憶體
- 靜態記憶體:static成員以及任何定義在函式之外的變數
- 棧記憶體:一般區域性變數
- 堆記憶體(自由空間):動態分配的物件
靜態記憶體和棧記憶體中的變數由編譯器產生和銷燬,動態分配的物件在我們不再使用它時要由程式設計師顯式地銷燬
一、介紹
動態分配記憶體
new()
:為物件分配空間,並返回指向該物件的指標delete
:銷燬物件,並釋放與之相關的記憶體
使用智慧指標:定義在標頭檔案memory中
shared_ptr
:允許多個指標指向同一個物件unique_ptr
:“獨佔”所使用的物件weak_ptr
:伴隨類,弱引用,指向shared_ptr所管理的物件
和容器一樣,只能指標也是一種模板,需要給它傳入一個引數來指定型別
二、shared_ptr類
宣告shared_ptr:
shared_ptr<string> p1; //shared_ptr,可以指向string
shared_ptr<list<int>> p2; //shared_ptr,可以指向list<int>
使用方式與普通指標一致,解引用返回它所指向的物件,在條件表示式中檢查是否為空
//若p1不為空且指向一個空string
if(p1 && p1->empty()){
*p1 = "hi"; //對p1重新賦值
}
make_shared函式
make_shared<typename>(arguments)
在動態記憶體中分配並初始化一個物件
返回指向此物件的shared_ptr指標
//指向一個值為42的int的shared_ptr
shared_ptr<int> p1 = make_shared<int>(42);
//指向一個值為"999"的string的shared_ptr
shared_ptr<string> p2 = make_shared<string>(3, '9');
//指向一個值為0的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>();
沒有傳入引數時,進行值初始化
auto p4 = make_shared<string>(); //p4指向空string
shared_ptr的拷貝和引用
每個share_ptr都有一個關聯的計數器
- 當拷貝shared_ptr時,計數器會遞增
- 當shared_ptr被賦予新值或者shared_ptr被銷燬(如一個區域性的shared_ptr離開其作用域),計數器會遞減
- 當一個shared_ptr的計數器==0時,記憶體會被釋放
auto r = make_shared<int>(42);
r = q; //給r賦值,使它指向另一個地址
//遞增q指向的物件的引用計數
//遞減r指向的物件的引用計數
//如果計數器為0,自動釋放
shared_ptr自動銷燬所管理的物件…
和其他類一樣,shared_ptr型別也有解構函式
shared_ptr的解構函式會
- 遞減指標所指向的物件的引用計數
- 當物件的引用計數為0時,銷燬物件並釋放記憶體
…shared_ptr還會自動釋放相關聯物件的記憶體
舉例:
//factory返回一個share_ptr,指向一個動態分配的物件
shared_ptr<Foo> factory(T arg){
//對arg的操作
return make_shared<Foo>(arg);
}
void ues_factory(T arg){
shared_ptr<Foo> p = factory(arg);
//使用p
}
//p離開了作用域,由於引用計數由1減到0,物件被銷燬,記憶體釋放
如果有其他引用計數也指向該物件,則物件記憶體不會被釋放掉
//factory和上述一致
//ues_factory返回shared_ptr的拷貝
void use_factory(T arg){
shared_ptr<Foo> p = factory(arg);
//使用p
return p; //返回p的拷貝,此時遞增了計數器,引用數為2
}//p離開作用域,物件計數器引用2-1=1,物件記憶體沒有釋放
return shared_ptr時,如果不是返回引用型別,則會進行拷貝,shared_ptr的計數器+1後-1,最終shared的計數器不變
由於在最後一個shared _ptr銷燬前記憶體都不會釋放,保證shared_ptr在無用之後不再保留就非常重要了。如果你忘記了銷燬程式不再需要的shared_ptr,程式仍會正確執行,但會浪費記憶體。
share_ptr 在無用之後仍然保留的一種可能情況是,你將shared _ptr存放在一個容器中,隨後重排了容器,從而不再需要某些元素。在這種情況下,你應該確保用erase刪除那些不再需要的shared_ptr元素。
如果你將shared ptr存放於一個容器中,而後不再需要全部元素,而只使用其中一部分,要記得用erase刪除不再需要的那些元素。
使用動態生存期的資源的類
程式使用動態記憶體的三種原因
- 程式不知道自己需要使用多少物件
- 不知道所需物件的準確型別
- 需要在多個物件間共享資料
容器類常出於第一種原因使用動態記憶體,在15章會看見出於第二種原因的例子,本節討論第三種原因
先考慮這麼一種情況:
我們要定義一個Blob類,當該型別的物件拷貝時,物件共享底層資料。
如b2 = b1時,b2,b1共享底層資料,對b2的操作也會印象到b1,且銷燬b2時,b1的仍指向原資料
Blob<string> b1; //空Blob
{
//新作用域
Blob<string> b2 = {"a","an","the"};
b1 = b2; //b1和b2共享資料
}//b2離開作用域,被銷燬了,但b2的資料不能被銷燬
//b1指向b2的原資料
應用舉例:Blob類
定義Blob類
最終,我們希望將Blob定義為一個模板類,但現在我們先將其定義為StrBlob,即底層資料是vector<string>的Blob
class StrBlob{
public:
//拷貝控制
StrBlob();//預設建構函式
StrBlob(initializer_list<string> il); //列表初始化
StrBlob(const StrBlob& strb);
//查詢
int size() const {return data->size();}
bool empty() const {return data->empty();}
//新增和刪除元素
void push_back(const string &t) {data->push_back(t);}
void pop_back() {data->pop_back();}
//訪問元素
string& front();
string& back();
private:
shared_ptr<vector<string>> data;
//如果data[i]不合法,丟擲異常
void check(int i, const string &msg) const;
};
StrBlob的建構函式
StrBlob::StrBlob() : data(make_shared<vector<string>>())
{cout<<"in StrBlob dafault"<<endl;};
StrBlob::StrBlob(initializer_list<string> il) :
data(make_shared<vector<string>>(il))
{cout<<"in StrBlob initializer_list"<<endl;}
元素訪問成員函式
在訪問時必須保證容器非空,定義check函式進行檢查
void StrBlob::check(int i, const string& msg) const{
if(i >= data->size())
throw out_of_range(msg);
}
元素訪問成員函式:
string& StrBlob::front(){
//如果vector為空,check會丟擲一個異常
check(0, "front on empty StrBlob");
return data->front();
}
string& StrBlob::back(){
check(0, "back on empty StrBlob");
return data->back();
}
StrBlob的拷貝、賦值和銷燬
StrBlob使用預設的拷貝、賦值和解構函式對此型別的物件進行操作
當我們對StrBlob物件進行拷貝、賦值和銷燬時,它的shared_ptr成員也會預設地進行拷貝、賦值和銷燬
//由於data是private的
//在StrBlob中設定一個介面look_data
//look_data返回data的引用
class StrBlob{
public:
//...
shared_ptr<vector<string>>& look_data()
{return data;} //返回引用,避免物件拷貝
private:
//其餘部分都不變
};
測試程式:
//測試程式
int main(){
StrBlob b1;
{//新作用域
StrBlob b2 = {"first element","second element"};
cout<<"before assignment : "
<<b2.look_data().use_count()<<endl;
b1 = b2;
cout<<"after assignment : "
<<b2.look_data().use_count()<<endl;
}//b2被銷燬,計數器遞減
//b1仍指向b2的原資料
cout<<b1.front()<<endl;
//列印此時b1的計數器
cout<<"b2 has been dstoryed : "
<<b1.look_data().use_count()<<endl;
return 0;
}
輸出結果:
如果
look_data
用值返回,而不是引用返回,那麼會存在拷貝【見6.2.2節筆記】,所有計數器的值會+1
三、直接管理記憶體
使用new分配記憶體
new
分配動態記憶體delete
銷燬動態記憶體
new和delete與智慧指標不同,類物件的拷貝、賦值和銷燬操作都不會預設地對動態分配的物件進行管理,無論是物件的建立還是銷燬,都需要程式設計師顯式地操作,在大型的應用場景中會十分複雜。
在熟悉C++拷貝控制之前,儘量只使用智慧指標,而不是本節的方法管理動態記憶體
使用new動態分配和初始化物件
new type_name
:返回一個指向該物件的指標
//pi指向一個動態分配,預設初始化的無名物件
int *pi = new int;
//*pi的值是未定義的
cout<<*pi<<endl;
物件是預設初始化這意味著:
指向的是:內建型別和組合型別物件。物件的值是未定義的
指向的是:類型別物件。呼叫預設建構函式
可以直接初始化動態分配的物件
- 直接呼叫建構函式
- 列表初始化
//pi指向物件的值為42
int *pi = new int(42);
//"9999999999"
string *ps = new string(10, '9');
//vector有5個元素,依次為0,1,2,3,4
vector<int> *pv = new vector<int>{0,1,2,3,4};
也可以值初始化
string *ps1 = new string(); //值初始化為空string
string *ps = new string; //預設初始化為空string
int *pi1 = new int; //預設初始化,值未定義
int *pi = new int(); //值初始化,*pi = 0;
所以,初始化動態分配的物件是一個好習慣
動態分配const物件
用new
可以分配const物件
和其他const物件一樣,動態分配的const物件必須被初始化
//分配並初始化const int
const int *pi = new const int(1024);
//分配並預設初始化const string
const string *ps = new const string;
記憶體耗盡
如果new分配動態記憶體失敗,返回一個空指標,並報出std::bad_alloc異常
int *p1 = new int; //返回空指標,丟擲異常
int *p2 = new (nothrow) int; //如果分配失敗,new返回空指標
我們第二種形式的new為定位new (placement new),其原因我們將在19.1.2節(第729頁)中解釋。
定位new表示式允許我們向new傳遞額外的引數。
在此例中,我們傳遞給它一個由標準庫定義的名為nothrow的物件。如果將nothrow傳遞給new,我們的意圖是告訴它不能丟擲異常。如果這種形式的 new不能分配所需記憶體,它會返回一個空指標。bad_alloc和nothrow都定義在標頭檔案new中。
使用delete釋放記憶體
基本介紹
delete()
:接受一個指標,指向我們想要銷燬的物件
執行兩個操作
- 銷燬物件
- 釋放對應的記憶體
注意點:
- 保證只傳給delete動態分配的指標,將一般指標傳給delete,其行為是未定義的
- 同一塊記憶體不能釋放兩次
- 不要忘記delete記憶體
- 不要使用已經delete的物件
int i, *pi = &i;
int *pd = new int();
delete pd; //正確:釋放pd記憶體
pd = nullptr; //好習慣:指出pd不再指向動態記憶體
delete pi; //未定義:pi沒有指向動態分配的記憶體
delete pd; //未定義:pd記憶體已經被釋放
保證以上兩點是程式設計師的責任,編譯器並不會檢查以上錯誤
舉例
在被顯式地delete前,用new動態分配的記憶體一直存在
Foo* factory(T arg){
//處理arg
return new Foo(arg);
}//呼叫者負責釋放
void ues_factory(T arg){
Foo *p = factory(arg);
//使用p但不delete它
}//p離開了作用域,但它所指向的記憶體沒有被釋放!!
use_factory返回時,區域性變數p被銷燬。但此變數是一個內建指標,而不是一個智慧指標,所以p所指向的記憶體並沒有被銷燬。
這樣就產生了一塊無名的記憶體塊,存在又無法刪除。
這也體現了智慧指標與普通指標的區別:智慧指標在離開自己的作用域,自己的變數名失效時,銷燬指向的物件並釋放關聯記憶體;而new產生的指標不會。
修改use_factory:
void use_factory(T arg){
Foo *p = factory(arg);
//使用p
delete p; //記得釋放p
}
堅持使用智慧指標,可以避免上述的絕大部分問題
四、shared_ptr和new結合使用
new直接初始化share_ptr
可以用new返回的指標初始化share_ptr
該建構函式是explicit的
所以,不存在new產生的指標向shared_ptr的隱式型別轉換,必須採用直接初始化,而不是拷貝初始化或者賦值
shared_ptr<int> p1(new int(42)); //正確:使用直接初始化
shared_ptr<int> p2 = new int(30);//錯誤:new產生的指標
同理,返回shared_ptr的函式不能返回new產生的指標
shared_ptr<int> clone(int p){
//錯誤:建構函式為explicit,無法轉換
//return new int(p);
//正確:顯式地用int*構造shared_ptr<int>
return shared_ptr<int>(new int(p));
}
如對隱式型別轉換有疑問檢視 7-5筆記第三點”隱式類型別轉換”
初始化時傳入可呼叫物件代替delete
預設情況下,一個用來初始化智慧指標的普通指標必須指向動態記憶體,因為智慧指標預設使用delete釋放它所關聯的物件。我們可以將智慧指標繫結到一個指向其他型別的資源的指標上,但是為了這樣做,必須提供自己的操作來替代 delete。我們將在12.1.4節介紹如何定義自己的釋放操作。
五、unique_ptr
和shared_ptr不同,某個時刻只能有一個unique_ptr指向一個給定物件
基本操作
必須採用直接初始化
unique_ptr<double> p1; //可以指向double的一個unique_ptr
unique_ptr<int> p2(new int(42)); //p2指向一個值為42的int
unique_ptr不支援拷貝與賦值
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1); //錯誤:不支援拷貝
unique_ptr<string> p3;
p3 = p1; //錯誤:不支援賦值
unique_ptr支援的操作
可以使用release和reset將指標的所有權從一個(非const)unique_ptr轉移到另一個unique_ptr
//將所有權從p1,轉移到p2
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1.release()); //release將p1置空
cout<<*p2<<endl; //輸出 hello
unique_ptr<string> p3(new string("world"));
//p2繫結的物件被釋放,p3置空,p2指向p3原來指向的物件
p2.reset(p3.release());
cout<<*p2<<endl; //輸出: world
傳遞和返回unique_ptr
不能拷貝unique_ptr 的規則有一個例外:我們可以拷貝或賦值一個將要被銷燬的unique_ptr。最常見的例子是從函式返回一個unique_ptr:
unique_ptr<int> clone(int p){
//正確:從int*建立一個unique_ptr<int>
return unique_ptr<int>(new int(p));
}
還可以返回一個區域性變數的拷貝
unique_ptr<int> clone(int p){
unique_ptr<int> ret(new int(p));
return ret;
}
對於兩段程式碼,編譯器都知道要返回的物件將要被銷燬。在此情況下,編譯器執行一種特殊的“拷貝”,我們將在13.6.2節(移動建構函式和移動運算子)中介紹它。
向unique_ptr傳遞刪除器
//p指向一個型別為objT的物件
//並使用一個型別為delT的可呼叫物件釋放objT
//p會使用一個名為fcnd的delT物件來刪除objT
unique_ptr<objT, delT> p(new objT, fcn);
作為一個更具體的例子,我們將寫一個連線程式,用unique_ptr來代替shared_ptr,如下所示:
void f(destination &d /*其他需要的引數*/)
{
connection c = connect(&d);//開啟連結
unique_ptr<connection, decltype(end_connection)*>
p(&c, end_connection);
//使用連結
//當f退出時(即使是由於異常而退出)
//connection會呼叫end_connection正常退出
}
注意decltype(end_connection)
返回一個函式型別,而函式型別不能作為引數,函式指標可以
所以要加上*
表示函式指標
p(&c, end_connection)
中,類似於陣列名錶示指標一樣,函式名實際上就表示函式指標
所以也可寫作p(&c, &end_connection)
,但沒必要。【前一個&表示引用傳遞,後一個&表示取址得到指標】