c++動態(tài)內(nèi)存管理與智能指針的相關(guān)知識點(diǎn)
引言
程序使用三種不同的內(nèi)存
靜態(tài)內(nèi)存:static成員以及任何定義在函數(shù)之外的變量棧內(nèi)存:一般局部變量堆內(nèi)存(自由空間):動態(tài)分配的對象
靜態(tài)內(nèi)存和棧內(nèi)存中的變量由編譯器產(chǎn)生和銷毀,動態(tài)分配的對象在我們不再使用它時要由程序員顯式地銷毀
一、介紹
動態(tài)分配內(nèi)存
- new():為對象分配空間,并返回指向該對象的指針
- delete:銷毀對象,并釋放與之相關(guān)的內(nèi)存
使用智能指針:定義在頭文件memory中
- shared_ptr:允許多個指針指向同一個對象
- unique_ptr:“獨(dú)占”所使用的對象
- weak_ptr:伴隨類,弱引用,指向shared_ptr所管理的對象
和容器一樣,只能指針也是一種模板,需要給它傳入一個參數(shù)來指定類型
二、shared_ptr類
聲明shared_ptr:
shared_ptr<string> p1; //shared_ptr,可以指向string shared_ptr<list<int>> p2; //shared_ptr,可以指向list<int>
使用方式與普通指針一致,解引用返回它所指向的對象,在條件表達(dá)式中檢查是否為空
//若p1不為空且指向一個空string if(p1 && p1->empty()){ *p1 = "hi"; //對p1重新賦值 }
make_shared函數(shù)
make_shared<typename>(arguments)
在動態(tài)內(nèi)存中分配并初始化一個對象
返回指向此對象的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>();
沒有傳入?yún)?shù)時,進(jìn)行值初始化
auto p4 = make_shared<string>(); //p4指向空string
shared_ptr的拷貝和引用
每個share_ptr都有一個關(guān)聯(lián)的計數(shù)器
當(dāng)拷貝shared_ptr時,計數(shù)器會遞增
當(dāng)shared_ptr被賦予新值或者shared_ptr被銷毀(如一個局部的shared_ptr離開其作用域),計數(shù)器會遞減
當(dāng)一個shared_ptr的計數(shù)器==0時,內(nèi)存會被釋放
auto r = make_shared<int>(42); r = q; //給r賦值,使它指向另一個地址 //遞增q指向的對象的引用計數(shù) //遞減r指向的對象的引用計數(shù) //如果計數(shù)器為0,自動釋放
shared_ptr自動銷毀所管理的對象…
和其他類一樣,shared_ptr類型也有析構(gòu)函數(shù)
shared_ptr的析構(gòu)函數(shù)會
- 遞減指針?biāo)赶虻膶ο蟮囊糜嫈?shù)
- 當(dāng)對象的引用計數(shù)為0時,銷毀對象并釋放內(nèi)存…shared_ptr還會自動釋放相關(guān)聯(lián)對象的內(nèi)存
舉例:
//factory返回一個share_ptr,指向一個動態(tài)分配的對象 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離開了作用域,由于引用計數(shù)由1減到0,對象被銷毀,內(nèi)存釋放
如果有其他引用計數(shù)也指向該對象,則對象內(nèi)存不會被釋放掉
//factory和上述一致 //ues_factory返回shared_ptr的拷貝 void use_factory(T arg){ shared_ptr<Foo> p = factory(arg); //使用p return p; //返回p的拷貝,此時遞增了計數(shù)器,引用數(shù)為2 }//p離開作用域,對象計數(shù)器引用2-1=1,對象內(nèi)存沒有釋放
return shared_ptr時,如果不是返回引用類型,則會進(jìn)行拷貝,shared_ptr的計數(shù)器+1后-1,最終shared的計數(shù)器不變
由于在最后一個shared _ptr銷毀前內(nèi)存都不會釋放,保證shared_ptr在無用之后不再保留就非常重要了。如果你忘記了銷毀程序不再需要的shared_ptr,程序仍會正確執(zhí)行,但會浪費(fèi)內(nèi)存。
share_ptr 在無用之后仍然保留的一種可能情況是,你將shared _ptr存放在一個容器中,隨后重排了容器,從而不再需要某些元素。在這種情況下,你應(yīng)該確保用erase刪除那些不再需要的shared_ptr元素。
如果你將shared ptr存放于一個容器中,而后不再需要全部元素,而只使用其中一部分,要記得用erase刪除不再需要的那些元素。
使用動態(tài)生存期的資源的類
程序使用動態(tài)內(nèi)存的三種原因
- 程序不知道自己需要使用多少對象
- 不知道所需對象的準(zhǔn)確類型
- 需要在多個對象間共享數(shù)據(jù)
容器類常出于第一種原因使用動態(tài)內(nèi)存,在15章會看見出于第二種原因的例子,本節(jié)討論第三種原因
先考慮這么一種情況:
我們要定義一個Blob類,當(dāng)該類型的對象拷貝時,對象共享底層數(shù)據(jù)。
如b2 = b1時,b2,b1共享底層數(shù)據(jù),對b2的操作也會印象到b1,且銷毀b2時,b1的仍指向原數(shù)據(jù)
Blob<string> b1; //空Blob { //新作用域 Blob<string> b2 = {"a","an","the"}; b1 = b2; //b1和b2共享數(shù)據(jù) }//b2離開作用域,被銷毀了,但b2的數(shù)據(jù)不能被銷毀 //b1指向b2的原數(shù)據(jù)
應(yīng)用舉例:Blob類
定義Blob類
最終,我們希望將Blob定義為一個模板類,但現(xiàn)在我們先將其定義為StrBlob,即底層數(shù)據(jù)是vector<string>的Blob
class StrBlob{ public: //拷貝控制 StrBlob();//默認(rèn)構(gòu)造函數(shù) 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的構(gòu)造函數(shù)
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;}
元素訪問成員函數(shù)
在訪問時必須保證容器非空,定義check函數(shù)進(jìn)行檢查
void StrBlob::check(int i, const string& msg) const{ if(i >= data->size()) throw out_of_range(msg); }
元素訪問成員函數(shù):
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使用默認(rèn)的拷貝、賦值和析構(gòu)函數(shù)對此類型的對象進(jìn)行操作
當(dāng)我們對StrBlob對象進(jìn)行拷貝、賦值和銷毀時,它的shared_ptr成員也會默認(rèn)地進(jìn)行拷貝、賦值和銷毀
//由于data是private的 //在StrBlob中設(shè)置一個接口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被銷毀,計數(shù)器遞減 //b1仍指向b2的原數(shù)據(jù) cout<<b1.front()<<endl; //打印此時b1的計數(shù)器 cout<<"b2 has been dstoryed : " <<b1.look_data().use_count()<<endl; return 0; }
輸出結(jié)果:
如果look_data用值返回,而不是引用返回,那么會存在拷貝【見6.2.2節(jié)筆記】,所有計數(shù)器的值會+1
三、直接管理內(nèi)存
使用new分配內(nèi)存
- new分配動態(tài)內(nèi)存
- delete銷毀動態(tài)內(nèi)存
new和delete與智能指針不同,類對象的拷貝、賦值和銷毀操作都不會默認(rèn)地對動態(tài)分配的對象進(jìn)行管理,無論是對象的創(chuàng)建還是銷毀,都需要程序員顯式地操作,在大型的應(yīng)用場景中會十分復(fù)雜。
在熟悉C++拷貝控制之前,盡量只使用智能指針,而不是本節(jié)的方法管理動態(tài)內(nèi)存
使用new動態(tài)分配和初始化對象
new type_name:返回一個指向該對象的指針
//pi指向一個動態(tài)分配,默認(rèn)初始化的無名對象 int *pi = new int; //*pi的值是未定義的 cout<<*pi<<endl;
對象是默認(rèn)初始化這意味著:
- 指向的是:內(nèi)置類型和組合類型對象。對象的值是未定義的
- 指向的是:類類型對象。調(diào)用默認(rèn)構(gòu)造函數(shù)
可以直接初始化動態(tài)分配的對象
- 直接調(diào)用構(gòu)造函數(shù)
- 列表初始化
//pi指向?qū)ο蟮闹禐?2 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; //默認(rèn)初始化為空string int *pi1 = new int; //默認(rèn)初始化,值未定義 int *pi = new int(); //值初始化,*pi = 0;
所以,初始化動態(tài)分配的對象是一個好習(xí)慣
動態(tài)分配const對象
用new可以分配const對象
和其他const對象一樣,動態(tài)分配的const對象必須被初始化
//分配并初始化const int const int *pi = new const int(1024); //分配并默認(rèn)初始化const string const string *ps = new const string;
內(nèi)存耗盡
如果new分配動態(tài)內(nèi)存失敗,返回一個空指針,并報出std::bad_alloc異常
int *p1 = new int; //返回空指針,拋出異常 int *p2 = new (nothrow) int; //如果分配失敗,new返回空指針
我們第二種形式的new為定位new (placement new),其原因我們將在19.1.2節(jié)(第729頁)中解釋。
定位new表達(dá)式允許我們向new傳遞額外的參數(shù)。
在此例中,我們傳遞給它一個由標(biāo)準(zhǔn)庫定義的名為nothrow的對象。如果將nothrow傳遞給new,我們的意圖是告訴它不能拋出異常。如果這種形式的 new不能分配所需內(nèi)存,它會返回一個空指針。bad_alloc和nothrow都定義在頭文件new中。
使用delete釋放內(nèi)存
基本介紹
delete():接受一個指針,指向我們想要銷毀的對象
執(zhí)行兩個操作
- 銷毀對象
- 釋放對應(yīng)的內(nèi)存
注意點(diǎn):
- 保證只傳給delete動態(tài)分配的指針,將一般指針傳給delete,其行為是未定義的
- 同一塊內(nèi)存不能釋放兩次
- 不要忘記delete內(nèi)存
- 不要使用已經(jīng)delete的對象
int i, *pi = &i; int *pd = new int(); delete pd; //正確:釋放pd內(nèi)存 pd = nullptr; //好習(xí)慣:指出pd不再指向動態(tài)內(nèi)存 delete pi; //未定義:pi沒有指向動態(tài)分配的內(nèi)存 delete pd; //未定義:pd內(nèi)存已經(jīng)被釋放
保證以上兩點(diǎn)是程序員的責(zé)任,編譯器并不會檢查以上錯誤
舉例
在被顯式地delete前,用new動態(tài)分配的內(nèi)存一直存在
Foo* factory(T arg){ //處理arg return new Foo(arg); }//調(diào)用者負(fù)責(zé)釋放 void ues_factory(T arg){ Foo *p = factory(arg); //使用p但不delete它 }//p離開了作用域,但它所指向的內(nèi)存沒有被釋放??!
use_factory返回時,局部變量p被銷毀。但此變量是一個內(nèi)置指針,而不是一個智能指針,所以p所指向的內(nèi)存并沒有被銷毀。
這樣就產(chǎn)生了一塊無名的內(nèi)存塊,存在又無法刪除。
這也體現(xiàn)了智能指針與普通指針的區(qū)別:智能指針在離開自己的作用域,自己的變量名失效時,銷毀指向的對象并釋放關(guān)聯(lián)內(nèi)存;而new產(chǎn)生的指針不會。
修改use_factory:
void use_factory(T arg){ Foo *p = factory(arg); //使用p delete p; //記得釋放p }
堅(jiān)持使用智能指針,可以避免上述的絕大部分問題
四、shared_ptr和new結(jié)合使用
new直接初始化share_ptr
可以用new返回的指針初始化share_ptr
該構(gòu)造函數(shù)是explicit的
所以,不存在new產(chǎn)生的指針向shared_ptr的隱式類型轉(zhuǎn)換,必須采用直接初始化,而不是拷貝初始化或者賦值
shared_ptr<int> p1(new int(42)); //正確:使用直接初始化 shared_ptr<int> p2 = new int(30);//錯誤:new產(chǎn)生的指針
同理,返回shared_ptr的函數(shù)不能返回new產(chǎn)生的指針
shared_ptr<int> clone(int p){ //錯誤:構(gòu)造函數(shù)為explicit,無法轉(zhuǎn)換 //return new int(p); //正確:顯式地用int*構(gòu)造shared_ptr<int> return shared_ptr<int>(new int(p)); }
如對隱式類型轉(zhuǎn)換有疑問查看 7-5筆記第三點(diǎn)”隱式類類型轉(zhuǎn)換”
初始化時傳入可調(diào)用對象代替delete
默認(rèn)情況下,一個用來初始化智能指針的普通指針必須指向動態(tài)內(nèi)存,因?yàn)橹悄苤羔樐J(rèn)使用delete釋放它所關(guān)聯(lián)的對象。我們可以將智能指針綁定到一個指向其他類型的資源的指針上,但是為了這樣做,必須提供自己的操作來替代 delete。我們將在12.1.4節(jié)介紹如何定義自己的釋放操作。
五、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將指針的所有權(quán)從一個(非const)unique_ptr轉(zhuǎn)移到另一個unique_ptr
//將所有權(quán)從p1,轉(zhuǎn)移到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 的規(guī)則有一個例外:我們可以拷貝或賦值一個將要被銷毀的unique_ptr。最常見的例子是從函數(shù)返回一個unique_ptr:
unique_ptr<int> clone(int p){ //正確:從int*創(chuàng)建一個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; }
對于兩段代碼,編譯器都知道要返回的對象將要被銷毀。在此情況下,編譯器執(zhí)行一種特殊的“拷貝”,我們將在13.6.2節(jié)(移動構(gòu)造函數(shù)和移動運(yùn)算符)中介紹它。
向unique_ptr傳遞刪除器
//p指向一個類型為objT的對象 //并使用一個類型為delT的可調(diào)用對象釋放objT //p會使用一個名為fcnd的delT對象來刪除objT unique_ptr<objT, delT> p(new objT, fcn);
作為一個更具體的例子,我們將寫一個連接程序,用unique_ptr來代替shared_ptr,如下所示:
void f(destination &d /*其他需要的參數(shù)*/) { connection c = connect(&d);//打開鏈接 unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection); //使用鏈接 //當(dāng)f退出時(即使是由于異常而退出) //connection會調(diào)用end_connection正常退出 }
注意decltype(end_connection)
返回一個函數(shù)類型,而函數(shù)類型不能作為參數(shù),函數(shù)指針可以
所以要加上*表示函數(shù)指針p(&c, end_connection)
中,類似于數(shù)組名表示指針一樣,函數(shù)名實(shí)際上就表示函數(shù)指針
所以也可寫作p(&c, &end_connection)
,但沒必要?!厩耙粋€&表示引用傳遞,后一個&表示取址得到指針】
總結(jié)
到此這篇關(guān)于c++動態(tài)內(nèi)存管理與智能指針的文章就介紹到這了,更多相關(guān)c++動態(tài)內(nèi)存管理與智能指針內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- C/C++ 傳遞動態(tài)內(nèi)存的深入理解
- C++動態(tài)內(nèi)存分配超詳細(xì)講解
- 詳解C++的靜態(tài)內(nèi)存分配與動態(tài)內(nèi)存分配
- C++ 中繼承與動態(tài)內(nèi)存分配的詳解
- 詳解C++動態(tài)內(nèi)存管理
- c++ 動態(tài)內(nèi)存分配相關(guān)總結(jié)
- 一文搞懂C++ 動態(tài)內(nèi)存
- C++使用動態(tài)內(nèi)存分配的原因解說
- C++中為什么要使用動態(tài)內(nèi)存
- c++動態(tài)內(nèi)存管理詳解(new/delete)
- C++中動態(tài)內(nèi)存管理的實(shí)現(xiàn)
相關(guān)文章
C語言學(xué)習(xí)進(jìn)階篇之萬字詳解指針與qsort函數(shù)
之前的指針詳解中,提到過qsort函數(shù),這個函數(shù)是用來排序的,下面這篇文章主要給大家介紹了關(guān)于C語言指針與qsort函數(shù)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08深入理解c++中char*與wchar_t*與string以及wstring之間的相互轉(zhuǎn)換
本篇文章是對c++中的char*與wchar_t*與string以及wstring之間的相互轉(zhuǎn)換進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05一篇文章帶你用C語言玩轉(zhuǎn)結(jié)構(gòu)體
本文主要介紹C語言 結(jié)構(gòu)體的知識,學(xué)習(xí)C語言肯定需要學(xué)習(xí)結(jié)構(gòu)體,這里詳細(xì)說明了結(jié)構(gòu)體并附示例代碼,供大家參考學(xué)習(xí),有需要的小伙伴可以參考下2021-09-09