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