記憶體管理之share_ptr
- 引子
- 初始化 sahred_ptr
- 關于get()函數
- 關于make_shared函數:
- shared_ptr的拷貝和指派
- 容器中的shared_ptr-記得用erease節省記憶體
- 狀态共享——why use shared_ptr?
- 智能指針與異常
-
- 1)管理動态數組
- 2)管理非正常動态對象
- 總結:
本文轉載自:記憶體管理之Share_Ptr
引子
C++中動态記憶體的管理是通過new和delete來完成的,隻要保證new和delete的配對使用,是沒有問題的。但是有時候我們會忘記釋放記憶體,甚至有時候我們根本就不知道什麼時候釋放記憶體。特别時在多個線程間共享資料時,更難判斷記憶體該何使釋放。這種情況下就機器容易産生引用非法記憶體的。
為了更容易(同時也更安全的管)的使用動态記憶體,新的标準庫(C++11)提供了兩種智能指針(smart pointer)類型來管理動态對象。智能指針的行為類似于正常指針。重要的差別是它負責自動釋放所指向的對象。新标準提供的這兩種智能指針的差別在于管理底層指針的方式:shared_ptr允許多個指針指向同一個對象;unique_ptr則獨占所指向的對象。标準庫還定義了一個weak_ptr的伴随類,他是一種弱引用,指向shared_ptr所管理的對象。這三種類型都定義在memory頭檔案中。
初始化 sahred_ptr
智能指針的使用方式與普通指針類似。解引用一個智能指針傳回它指向的對象。如果在一個條件判斷中使用智能指針,效果就是檢測它是否為空:
#include <iostream>
using namespace std;
int main()
{
/*---------空指針------------*/
shared_ptr<string> p1;
if(!p1) //!預設初始化的智能指針中儲存着一個空指針!并不是""空字元串
cout<<"p1==NULL"<<endl;
/*---------初始化------------*/
shared_ptr<string> p1(new string);
if(p1&&p1->empty()){ //!需要注意的時empty時屬于string的成員函數。
*p1="helloworld";
cout<<*p1<<endl;
}
// shared_ptr<int> pa = new int(1);//!error:不允許以暴露裸漏的指針進行指派操作。
//一般的初始化方式
shared_ptr<string> pint(new string("normal usage!"));
cout<<*pint<<endl;
//推薦的安全的初始化方式
shared_ptr<string> pint1 = make_shared<string>("safe uage!");
cout<<*pint1<<endl;
}
關于其它初始化智能指針的方法,如下;不推薦!在這裡展示出來是希望極力避免不安全的使用範例。
/*不推薦*/
int * p = new int(32);
shared_ptr<int> pp(p);
cout<<*pp<<endl;
/*意外的情況*/
// delete p; //!不小心把delete掉了。
// cout<<*pp<<endl;· //!pp也不再有效。
關于get()函數
智能指針定義了一個名為get的函數,它傳回一個内置指針,指向智能指針的管理的對象。此函數設定的初衷是當我們向不能使用智能指針的代碼傳遞一個内置指針。使用get傳回指針的代碼不能delete此指針。
#include <iostream>
#include <memory>
using namespace std;
void useShared_ptr(int *p)
{
cout<<*p<<endl;
}
void delePointer(int *p)
{
delete p;
}
int main(int argc, char *argv[])
{
shared_ptr<int> p1 = make_shared<int>(32);
// shared_ptr<int>p2(p1.get()); //!錯誤的用法:但是p1、p2各自保留了對一段記憶體的引用計數,其中有一個引用計數耗盡,資源也就釋放了。
useShared_ptr(p1.get());
// delePointer(p1.get()); //!error:
return 0;
}
再次聲明:get用來将指針的通路權限傳遞給代碼,隻有在确定代碼不會delete指針的情況下,才能使用get。特别是,永遠不要用get初始化另一個智能指針或者為另一個智能指針指派!
關于make_shared函數:
最安全的配置設定和使用動态記憶體的方法是調用一個名為make_shared的标準庫函數,此函數在動态記憶體中配置設定一個對象并初始化它,傳回此對象的shared_ptr。與智能指針一樣,make_ptr也定義在頭檔案memory中。
#include <iostream>
using namespace std;
int main()
{
shared_ptr<int> p3 = make_shared<int>(42);
cout<<*p3<<endl;
shared_ptr<string> pstr = make_shared<string>("99999");
cout<<*pstr<<endl;
shared_ptr<int> pint = make_shared<int>(); //!預設初始化為 0
cout<<*pint<<endl;
auto pau = make_shared<string>("auto"); //!更簡單,更常用的方式。
cout<<*pau<<endl;
}
使用make_shared用其參數來構造給定類型的對象;傳遞的參數必須能夠與該類型的某個構造函數相比對。
通常我們用auto來定義一個對象來儲存make_shared的結果,這種方式更為簡單。
shared_ptr的拷貝和指派
當進行拷貝或者指派操作時,每個shared_ptr都會記錄有多少個其他的shared_ptr指向相同的對象:
#include <iostream>
using namespace std;
int main()
{
auto p = make_shared<int>(42); //!p指向的對象隻有p一個引用者。
cout<<p.use_count()<<endl;
auto q(p); //!p和q指向相同的對象,此對象有兩個引用者。
cout<<p.use_count()<<endl;
return 0;
}
output:
1
2
shared_ptr作傳回值:
#include <iostream>
using namespace std;
shared_ptr<string> factory(const char* p){
return make_shared<string>(p);
}
void use_factory(){
shared_ptr<string> p = factory("helloworld");
cout<<*p<<endl; //!離開作用域時,p引用的對象被銷毀。
}
shared_ptr<string> return_share_ptr()
{
shared_ptr<string> p = factory("helloworld");
cout<<*p<<endl;
return p; //!傳回p時,引用計數進行了遞增操作。
} //!p離開了作用域,但他指向的記憶體不會被釋放掉。
int main()
{
use_factory();
auto p = return_share_ptr();
cout<<p.use_count()<<endl;
}
引用計數:
可以認為每個shared_ptr都有一個關聯的計數器,通常稱其為引用計數。無論何時我們拷貝一個shared_ptr,計數器都會遞增。例如,當用一個shared_ptr去初始化另一個shared_ptr;當我們給shared_ptr賦予一個新的值或者是shared_ptr被銷毀(例如一個局部的shared_ptr離開其作用域)時,計數器就會遞減。一旦一個shared_ptr的計數器變為0,他就會自動釋放自己所管理的對象。
#include <iostream>
using namespace std;
int main()
{
auto p = make_shared<int>(42); //!指向的對象隻有p一個引用者。
cout<<p.use_count()<<endl;
auto q = make_shared<int>(56);//!指向的對象隻有q一個引用者。
cout<<q.use_count()<<endl;
cout<<"---------afterAssin-----"<<endl;
p = q; //!p原來引用的對象經過指派之後釋放掉了,q引用的對象有了p和q兩個引用。
cout<<*p<<"=="<<*q<<endl;
cout<<q.use_count()<<endl;
}
其他shared_ptr操作
shared_ptr還定義了一些其他的操作,參考前面的shared_ptr操作表格,例如,我們可以用reset将一個 新的指針賦予一個shared_ptr:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<string> p1(new string("helloworld--1"));
// p1 = new string("helloworld2--2");//error!
p1.reset(new string("helloworld2--2"));
cout<<*p1<<endl;
}
與指派類似,reset會更新(-1)引用計數,如果需要的話,會釋放p1指向的對象。reset成員經常與unique一起使用,來控制多個shared_ptr的共享對象。在改變底層對象之前,我們在檢查自己是否是目前對象僅有的使用者。如果不是,在改變之前要做一份新的拷貝:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<string> p1(new string("helloworld--1"));
shared_ptr<string> p2(p1);
if(p1.unique())
cout<<*p1 + string(" is unique!")<<endl;
else{
p1.reset(new string("new reset!"));
cout<<*p1<<endl;
}
}
容器中的shared_ptr-記得用erease節省記憶體
對于一塊記憶體,shared_ptr類保證隻要有任何shared_ptr對象引用它,他就不會被釋放掉。由于這個特性,保證shared_ptr在不用之後不再保留就非常重要了,通常這個過程能夠自動執行而不需要人工幹預,有一種例外就是我們将shared_ptr放在了容器中。是以永遠不要忘記erease不用的shared_ptr。
#include <iostream>
using namespace std;
int main()
{
list<shared_ptr<string>>pstrList;
pstrList.push_back(make_shared<string>("1111"));
pstrList.push_back(make_shared<string>("2222"));
pstrList.push_back(make_shared<string>("3333"));
pstrList.push_back(make_shared<string>("4444"));
for(auto p:pstrList)
{
if(*p == "3333");
{
/*do some thing!*/
}
cout<<*p<<endl;
}
/*包含"3333"的資料我們已經使用完了!*/
list<shared_ptr<string>>::iterator itr = pstrList.begin();
for(;itr!=pstrList.end();++itr)
{
if(**itr == "3333"){
cout<<**itr<<endl;
pstrList.erase(itr);
}
}
cout<<"-------------after remove------------"<<endl;
for(auto p:pstrList)
{
cout<<*p<<endl;
}
while(1)
{
/*do somthing other works!*/
/*周遊 pstrList*/ //!這樣不僅節約了大量記憶體,也為容器的使用增加了效率
}
}
# output
1111
2222
3333
4444
3333
-------------after remove------------
1111
2222
4444
狀态共享——why use shared_ptr?
使用shared_ptr在一個常見的原因是允許多個多個對象共享相同的狀态,而非多個對象獨立的拷貝!
#include <iostream>
using namespace std;
void copyCase()
{
list<string> v1({"1","b","d"});
list<string> v2 = v1; //!v1==v2占用兩段記憶體
v1.push_back("cc"); //!v1!=v2
for(auto &p:v1){
cout<<p<<endl;
}
cout<<"--------void copyCase()---------"<<endl;
for(auto &p:v2){
cout<<p<<endl;
}
} //v1和v2分屬兩個不同的對象,一個改變不會影響的狀态。
void shareCase()
{
shared_ptr<list<string>> v1 = make_shared<list<string>>(2,"bb");
shared_ptr<list<string>> v2 = v1;
(*v1).push_back("c2c");
for(auto &p:*v1){
cout<<p<<endl;
}
cout<<"----------shareCase()--------"<<endl;
for(auto &p:*v2){
cout<<p<<endl;
}
} //v1和v2屬于一個對象的兩個引用,有引用計數為證,其内容的改變是統一的。
int main()
{
copyCase();
cout<<"++++++++++++++++"<<endl;
shareCase();
}
#output
1
b
d
cc
--------void copyCase()---------
1
b
d
++++++++++++++++
bb
bb
c2c
----------shareCase()--------
bb
bb
c2c
Program ended with exit code: 0
智能指針與異常
異常發生後,正常的動态記憶體常常不能正确釋放。但是如果使用智能指針,即程式過早結束,智能指針也能確定在記憶體不需要時将其釋放:
void f()
{
shared_ptr<int>sp(new int(42));
}
函數的退出,要麼有兩種情況,正常處理結束或者發生了異常,無論哪種情況,局部對象都會被銷毀。在上面的程式中,sp是一個shared_ptr,是以sp銷毀時會檢查引用計數。在此例中,sp是指向這塊記憶體的唯一指針。是以會被釋放掉。
與之相對的,當發生異常時,我們直接管理的記憶體時不會自動釋放的,如果使用内置指針管理記憶體,且在new之後對應的delete之前發生異常,則記憶體不會釋放。
void f()
{
int *p = new int(42);
//code//!異常抛出,且沒有在f()中被捕獲。
delete p;
}
如果在new和delete之間發生異常,且異常未在f()中捕獲,則記憶體就永遠不會被釋放了。
shared_ptr對象的銷毀
1)管理動态數組
預設情況下,shared_ptr指向的動态的記憶體是使用delete來删除的。這和我們手動去調用delete然後調用對象内部的析構函數是一樣的。與unique_ptr不同,shared_ptr不直接管理動态數組。如果希望使用shared_ptr管理一個動态數組,必須提供自定義的删除器來替代delete
#include <iostream>
using namespace std;
class DelTest
{
public:
DelTest(){
static int i = 0;
cout<<" DelTest()"<<":"<<i++<<endl;
}
~DelTest(){
static int i = 0;
cout<<"~ DelTest()"<<":"<<i++<<endl;
}
};
void noDefine()
{
cout<<"no_define start running!"<<endl;
shared_ptr<DelTest> p(new DelTest[10]);
}
void slefDefine()
{
cout<<"slefDefine start running!"<<endl;
shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});
} //!傳入lambada表達式代替delete操作。
int main()
{
noDefine(); //!構造10次,析構1次。記憶體洩漏。
cout<<"----------------------"<<endl;
slefDefine(); //!構造次數==析構次數 無記憶體洩漏
}
#output
no_define start running!
DelTest():0
DelTest():1
DelTest():2
DelTest():3
DelTest():4
DelTest():5
DelTest():6
DelTest():7
DelTest():8
DelTest():9
~ DelTest():0
ZXPro(68858,0x1000c35c0) malloc: *** error for object 0x100509788: pointer being freed was not allocated
ZXPro(68858,0x1000c35c0) malloc: *** set a breakpoint in malloc_error_break to debug
通過自定義删除器的方式shared_ptr雖然管理的是一個動态數組。但是shard_ptr并不支援标運算符的操作。而且智能指針類型不支援指針算術運算。是以為了通路數組中的元素,必須用get擷取一個内置指針,然後用它來通路數組元素。
2)管理非正常動态對象
某些情況下,有些動态記憶體也不是我們new出來的,如果要用shared_ptr管理這種動态記憶體,也要自定義删除器。
#include <iostream>
#include <stdio.h>
#include <memory>
using namespace std;
void closePf(FILE * pf)
{
cout<<"----close pf after works!----"<<endl;
fclose(pf);
}
int main()
{
// FILE * fp2 = fopen("bin2.txt", "w");
// if(!pf)
// return -1;
// char *buf = "abcdefg";
// fwrite(buf, 8, 1, fp2);
// fclose(fp2);
shared_ptr<FILE> pf(fopen("bin2.txt", "w"),closePf);
cout<<"*****start working****"<<endl;
if(!pf)
return -1;
char *buf = "abcdefg";
fwrite(buf, 8, 1, pf.get()); //!確定fwrite不會删除指針的情況下,可以将shared_ptr内置指針取出來。
cout<<"----write int file!-----"<<endl;
} //!即可以避免異常發生後無法釋放記憶體的問題,也避免了很多人忘記執行fclose的問題。
在這裡可以設想一下TCP/IP中連結的打開和關閉的情況,同理都可以使用智能指針來管理。
總結:
最後總結一下上面所陳述的内容,也是shared_ptr使用的基本規範
1)不使用相同的内置指針值初始化(或reset)多個智能指針。
2)不delete get函數傳回的指針。
3)如果你使用了get傳回的指針,記住當最後一個對應的智能指針銷毀後,你的指針就變為無效了。
4)如果你使用智能指針管理的資源不是new配置設定的記憶體,記得傳遞給他一個删除器。
weak_ptr
weakptr使用的比較少,如有興趣了解,請去參考該篇文章:https://www.cnblogs.com/DswCnblog/p/5628314.html