c++動態(tài)內(nèi)存管理與智能指針的相關(guā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:“獨占”所使用的對象
- 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>
使用方式與普通指針一致,解引用返回它所指向的對象,在條件表達式中檢查是否為空
//若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ù)時,進行值初始化
auto p4 = make_shared<string>(); //p4指向空string
shared_ptr的拷貝和引用
每個share_ptr都有一個關(guān)聯(lián)的計數(shù)器
當拷貝shared_ptr時,計數(shù)器會遞增
當shared_ptr被賦予新值或者shared_ptr被銷毀(如一個局部的shared_ptr離開其作用域),計數(shù)器會遞減
當一個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ù)會
- 遞減指針所指向的對象的引用計數(shù)
- 當對象的引用計數(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時,如果不是返回引用類型,則會進行拷貝,shared_ptr的計數(shù)器+1后-1,最終shared的計數(shù)器不變
由于在最后一個shared _ptr銷毀前內(nèi)存都不會釋放,保證shared_ptr在無用之后不再保留就非常重要了。如果你忘記了銷毀程序不再需要的shared_ptr,程序仍會正確執(zhí)行,但會浪費內(nèi)存。
share_ptr 在無用之后仍然保留的一種可能情況是,你將shared _ptr存放在一個容器中,隨后重排了容器,從而不再需要某些元素。在這種情況下,你應該確保用erase刪除那些不再需要的shared_ptr元素。
如果你將shared ptr存放于一個容器中,而后不再需要全部元素,而只使用其中一部分,要記得用erase刪除不再需要的那些元素。
使用動態(tài)生存期的資源的類
程序使用動態(tài)內(nèi)存的三種原因
- 程序不知道自己需要使用多少對象
- 不知道所需對象的準確類型
- 需要在多個對象間共享數(shù)據(jù)
容器類常出于第一種原因使用動態(tài)內(nèi)存,在15章會看見出于第二種原因的例子,本節(jié)討論第三種原因
先考慮這么一種情況:
我們要定義一個Blob類,當該類型的對象拷貝時,對象共享底層數(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ù)應用舉例:Blob類
定義Blob類
最終,我們希望將Blob定義為一個模板類,但現(xiàn)在我們先將其定義為StrBlob,即底層數(shù)據(jù)是vector<string>的Blob
class StrBlob{
public:
//拷貝控制
StrBlob();//默認構(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ù)進行檢查
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使用默認的拷貝、賦值和析構(gòu)函數(shù)對此類型的對象進行操作
當我們對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被銷毀,計數(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與智能指針不同,類對象的拷貝、賦值和銷毀操作都不會默認地對動態(tài)分配的對象進行管理,無論是對象的創(chuàng)建還是銷毀,都需要程序員顯式地操作,在大型的應用場景中會十分復雜。
在熟悉C++拷貝控制之前,盡量只使用智能指針,而不是本節(jié)的方法管理動態(tài)內(nèi)存
使用new動態(tài)分配和初始化對象
new type_name:返回一個指向該對象的指針
//pi指向一個動態(tài)分配,默認初始化的無名對象 int *pi = new int; //*pi的值是未定義的 cout<<*pi<<endl;
對象是默認初始化這意味著:
- 指向的是:內(nèi)置類型和組合類型對象。對象的值是未定義的
- 指向的是:類類型對象。調(diào)用默認構(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; //默認初始化為空string int *pi1 = new int; //默認初始化,值未定義 int *pi = new int(); //值初始化,*pi = 0;
所以,初始化動態(tài)分配的對象是一個好習慣
動態(tài)分配const對象
用new可以分配const對象
和其他const對象一樣,動態(tài)分配的const對象必須被初始化
//分配并初始化const int const int *pi = new const int(1024); //分配并默認初始化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表達式允許我們向new傳遞額外的參數(shù)。
在此例中,我們傳遞給它一個由標準庫定義的名為nothrow的對象。如果將nothrow傳遞給new,我們的意圖是告訴它不能拋出異常。如果這種形式的 new不能分配所需內(nèi)存,它會返回一個空指針。bad_alloc和nothrow都定義在頭文件new中。
使用delete釋放內(nèi)存
基本介紹
delete():接受一個指針,指向我們想要銷毀的對象
執(zhí)行兩個操作
- 銷毀對象
- 釋放對應的內(nèi)存
注意點:
- 保證只傳給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; //好習慣:指出pd不再指向動態(tài)內(nèi)存 delete pi; //未定義:pi沒有指向動態(tài)分配的內(nèi)存 delete pd; //未定義:pd內(nèi)存已經(jīng)被釋放
保證以上兩點是程序員的責任,編譯器并不會檢查以上錯誤
舉例
在被顯式地delete前,用new動態(tài)分配的內(nèi)存一直存在
Foo* factory(T arg){
//處理arg
return new Foo(arg);
}//調(diào)用者負責釋放
void ues_factory(T arg){
Foo *p = factory(arg);
//使用p但不delete它
}//p離開了作用域,但它所指向的內(nèi)存沒有被釋放?。?/pre>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
}堅持使用智能指針,可以避免上述的絕大部分問題
四、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筆記第三點”隱式類類型轉(zhuǎn)換”
初始化時傳入可調(diào)用對象代替delete
默認情況下,一個用來初始化智能指針的普通指針必須指向動態(tài)內(nèi)存,因為智能指針默認使用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ù)和移動運算符)中介紹它。
向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);
//使用鏈接
//當f退出時(即使是由于異常而退出)
//connection會調(diào)用end_connection正常退出
}注意decltype(end_connection)返回一個函數(shù)類型,而函數(shù)類型不能作為參數(shù),函數(shù)指針可以
所以要加上*表示函數(shù)指針p(&c, end_connection)中,類似于數(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)存分配超詳細講解
- 詳解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)存管理的實現(xiàn)
相關(guān)文章
深入理解c++中char*與wchar_t*與string以及wstring之間的相互轉(zhuǎn)換
本篇文章是對c++中的char*與wchar_t*與string以及wstring之間的相互轉(zhuǎn)換進行了詳細的分析介紹,需要的朋友參考下2013-05-05
一篇文章帶你用C語言玩轉(zhuǎn)結(jié)構(gòu)體
本文主要介紹C語言 結(jié)構(gòu)體的知識,學習C語言肯定需要學習結(jié)構(gòu)體,這里詳細說明了結(jié)構(gòu)體并附示例代碼,供大家參考學習,有需要的小伙伴可以參考下2021-09-09

