智能指標的基礎知識
智能指標能做到保證資源的自動釋放
利用堆疊上物件離開作用自動析構的特性保證自動釋放,
//智能指標簡單的實作
template<typename T>
class CSmartPtr{
public:
CSmartPtr(T* ptr= nullptr):_mptr(ptr){
}
~CSmartPtr(){
delete _mptr;
}
T& operator*(){
return *_mptr;
}
T* operator->(){
return _mptr;
}
private:
T* _mptr;
};
不帶參考計數的智能指標
在使用智能指標的使用要先參考memory庫
stl中提供了三種不帶參考計數的智能指標:auto_ptr、scoped_ptr和unique_ptr
auto_ptr在使用程序中,如果進行賦值和拷貝運算時,會將第一個指標置空,讓拷貝或賦值的指標管理之前的記憶體,基本不使用這個
一個面試題:可以在vector中使用auto_ptr嗎?
? 答:最好不要,因為在使用容器的程序中,難免會發生拷貝(容器1復制到容器2),而auto_ptr的實作方式中為了避免淺拷貝,會把初始的指標置為空,讓賦值的指標管理這塊記憶體,之前的指標失去了堆記憶體的管理權,這樣再訪問容器1就會發現其中的都是空指標,
scoped_ptr中將拷貝建構式和賦值多載函式都洗掉了,從原始碼中禁止了拷貝和賦值,這個指標用的也比較少
推薦使用unique_ptr,與scoped_ptr相同,也是洗掉了拷貝構造和賦值多載函式,但是定義了移動建構式,在呼叫程序中需要使用move函式,可以對右值進行接管,好處是用戶可以感知之前的指標不能再對記憶體進行管理了,心中有數,
帶參考計數的智能指標
自己實作:
template<typename T>
class RefCnt {//計數類
public:
RefCnt<T>(T *ptr = nullptr) : _mptr(ptr) {
if (_mptr != nullptr) {
_mcount = 1;
}
}
void addRef() { _mcount++; }
int delRef() { return --_mcount; }
private:
T *_mptr;
int _mcount;
};
template<typename T>
class CSmartPtr {
public:
CSmartPtr(T *ptr = nullptr) : _mptr(ptr) {
_mRefPtr = new RefCnt<T>(_mptr);
}
~CSmartPtr() {
if (0 == _mRefPtr->delRef()) {
delete _mptr;
_mptr = nullptr;
}
}
CSmartPtr<T>(const CSmartPtr<T> &src)
: _mptr(src._mptr), _mRefPtr(src._mRefPtr) {
if (_mptr != nullptr)
_mRefPtr->addRef();
}
CSmartPtr<T> &operator=(const CSmartPtr<T> &src) {
if (this == &src) {
return *this;
}
if (0 == _mRefPtr->delRef()) {
delete _mptr;
}
_mptr = src._mptr;
_mRefPtr = src._mRefPtr;
_mRefPtr->addRef();
return *this;
}
T &operator*() {
return *_mptr;
}
T *operator->() {
return _mptr;
}
private:
T *_mptr;
RefCnt<T> *_mRefPtr;
};
shared_ptr交叉參考問題
stl中提供了兩個帶有參考計數的智能指標
shared_ptr:強智能指標,可以改變資源的參考計數
weak_ptr:弱智能指標,不會改變資源的參考計數
有這樣一例子,是shared_ptr的交叉參考:
#include <iostream>
#include <memory>
using namespace std;
class B; // 前置宣告類B
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
shared_ptr<B> _ptrb; // 指向B物件的智能指標
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
shared_ptr<A> _ptra; // 指向A物件的智能指標
};
int main()
{
shared_ptr<A> ptra(new A());// ptra指向A物件,A的參考計數為1
shared_ptr<B> ptrb(new B());// ptrb指向B物件,B的參考計數為1
ptra->_ptrb = ptrb;// A物件的成員變數_ptrb也指向B物件,B的參考計數為2
ptrb->_ptra = ptra;// B物件的成員變數_ptra也指向A物件,A的參考計數為2
cout << ptra.use_count() << endl; // 列印A的參考計數結果:2
cout << ptrb.use_count() << endl; // 列印B的參考計數結果:2
/*
出main函式作用域,ptra和ptrb兩個區域物件析構,分別給A物件和
B物件的參考計數從2減到1,達不到釋放A和B的條件(釋放的條件是
A和B的參考計數為0),因此造成兩個new出來的A和B物件無法釋放,
導致記憶體泄露,這個問題就是“強智能指標的交叉參考(回圈參考)問題”
*/
return 0;
}
代碼列印結果:
A()
B()
2
2
可以看到,A和B物件并沒有進行析構,通過上面的代碼示例,能夠看出來“交叉參考”的問題所在,就是物件無法析構,資源無法釋放,那怎么解決這個問題呢?請注意強弱智能指標的一個重要應用規則:定義物件時,用強智能指標shared_ptr,在其它地方參考物件時,使用弱智能指標weak_ptr,

弱智能指標weak_ptr區別于shared_ptr之處在于:
weak_ptr不會改變資源的參考計數,只是一個觀察者的角色,通過觀察shared_ptr來判定資源是否存在weak_ptr持有的參考計數,不是資源的參考計數,而是同一個資源的觀察者的計數weak_ptr沒有提供常用的指標操作,無法直接訪問資源,需要先通過lock方法提升為shared_ptr強智能指標,才能訪問資源
那么上面的代碼怎么修改,也就是如何解決帶參考計數的智能指標的交叉參考問題,代碼如下:
#include <iostream>
#include <memory>
using namespace std;
class B; // 前置宣告類B
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
weak_ptr<B> _ptrb; // 指向B物件的弱智能指標,參考物件時,用弱智能指標
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
weak_ptr<A> _ptra; // 指向A物件的弱智能指標,參考物件時,用弱智能指標
};
int main()
{
// 定義物件時,用強智能指標
shared_ptr<A> ptra(new A());// ptra指向A物件,A的參考計數為1
shared_ptr<B> ptrb(new B());// ptrb指向B物件,B的參考計數為1
// A物件的成員變數_ptrb也指向B物件,B的參考計數為1,因為是弱智能指標,參考計數沒有改變
ptra->_ptrb = ptrb;
// B物件的成員變數_ptra也指向A物件,A的參考計數為1,因為是弱智能指標,參考計數沒有改變
ptrb->_ptra = ptra;
cout << ptra.use_count() << endl; // 列印結果:1
cout << ptrb.use_count() << endl; // 列印結果:1
/*
出main函式作用域,ptra和ptrb兩個區域物件析構,分別給A物件和
B物件的參考計數從1減到0,達到釋放A和B的條件,因此new出來的A和B物件
被析構掉,解決了“強智能指標的交叉參考(回圈參考)問題”
*/
return 0;
}
代碼列印如下:
A()
B()
1
1
~B()
~A
可以看到,A和B物件正常析構,問題解決!
多執行緒訪問共享物件的執行緒安全問題
有一個用C++寫的開源網路庫,muduo庫,作者陳碩,大家可以在網上下載到muduo的源代碼,該原始碼中對于智能指標的應用非常優秀,其中借助shared_ptr和weak_ptr解決了這樣一個問題,多執行緒訪問共享物件的執行緒安全問題,解釋如下:執行緒A和執行緒B訪問一個共享的物件,如果執行緒A正在析構這個物件的時候,執行緒B又要呼叫該共享物件的成員方法,此時可能執行緒A已經把物件析構完了,執行緒B再去訪問該物件,就會發生不可預期的錯誤,
先看如下代碼:
#include <iostream>
#include <thread>
using namespace std;
class Test
{
public:
// 構造Test物件,_ptr指向一塊int堆記憶體,初始值是20
Test() :_ptr(new int(20))
{
cout << "Test()" << endl;
}
// 析構Test物件,釋放_ptr指向的堆記憶體
~Test()
{
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 該show會在另外一個執行緒中被執行
void show()
{
cout << *_ptr << endl;
}
private:
int *volatile _ptr;
};
void threadProc(Test *p)
{
// 睡眠兩秒,此時main主執行緒已經把Test物件給delete析構掉了
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
此時當前執行緒訪問了main執行緒已經析構的共享物件,結果未知,隱含bug,
此時通過p指標想訪問Test物件,需要判斷Test物件是否存活,如果Test物件
存活,呼叫show方法沒有問題;如果Test物件已經析構,呼叫show有問題!
*/
p->show();
}
int main()
{
// 在堆上定義共享物件
Test *p = new Test();
// 使用C++11的執行緒類,開啟一個新執行緒,并傳入共享物件的地址p
std::thread t1(threadProc, p);
// 在main執行緒中析構Test共享物件
delete p;
// 等待子執行緒運行結束
t1.join();
return 0;
}
運行上面的代碼,發現在main主執行緒已經delete析構Test物件以后,子執行緒threadProc再去訪問Test物件的show方法,無法列印出*_ptr的值20,可以用shared_ptr和weak_ptr來解決多執行緒訪問共享物件的執行緒安全問題,上面代碼修改如下:
#include <iostream>
#include <thread>
#include <memory>
using namespace std;
class Test
{
public:
// 構造Test物件,_ptr指向一塊int堆記憶體,初始值是20
Test() :_ptr(new int(20))
{
cout << "Test()" << endl;
}
// 析構Test物件,釋放_ptr指向的堆記憶體
~Test()
{
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 該show會在另外一個執行緒中被執行
void show()
{
cout << *_ptr << endl;
}
private:
int *volatile _ptr;
};
void threadProc(weak_ptr<Test> pw) // 通過弱智能指標觀察強智能指標
{
// 睡眠兩秒
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
如果想訪問物件的方法,先通過pw的lock方法進行提升操作,把weak_ptr提升
為shared_ptr強智能指標,提升程序中,是通過檢測它所觀察的強智能指標保存
的Test物件的參考計數,來判定Test物件是否存活,ps如果為nullptr,說明Test物件
已經析構,不能再訪問;如果ps!=nullptr,則可以正常訪問Test物件的方法,
*/
shared_ptr<Test> ps = pw.lock();
if (ps != nullptr)
{
ps->show();
}
}
int main()
{
// 在堆上定義共享物件
shared_ptr<Test> p(new Test);
// 使用C++11的執行緒,開啟一個新執行緒,并傳入共享物件的弱智能指標
std::thread t1(threadProc, weak_ptr<Test>(p));
// 在main執行緒中析構Test共享物件
// 等待子執行緒運行結束
t1.join();
return 0;
}
運行上面的代碼,show方法可以列印出20,因為main執行緒呼叫了t1.join()方法等待子執行緒結束,此時pw通過lock提升為ps成功,見上面代碼示例,
如果設定t1為分離執行緒,讓main主執行緒結束,p智能指標析構,進而把Test物件析構,此時show方法已經不會被呼叫,因為在threadProc方法中,pw提升到ps時,lock方法判定Test物件已經析構,提升失敗!main函式代碼可以如下修改測驗:
int main()
{
// 在堆上定義共享物件
shared_ptr<Test> p(new Test);
// 使用C++11的執行緒,開啟一個新執行緒,并傳入共享物件的弱智能指標
std::thread t1(threadProc, weak_ptr<Test>(p));
// 在main執行緒中析構Test共享物件
// 設定子執行緒分離
t1.detach();
return 0;
}
該main函式運行后,最終的threadProc中,show方法不會被執行到,以上是在多執行緒中訪問共享物件時,對shared_ptr和weak_ptr的一個典型應用,
自定義洗掉器
標準庫中的智能指標中默認的析構方法是delete p,如果使用智能指標指向陣列或者是檔案就需要自定義洗掉器,傳統的方式是通過自定義函式物件作為洗掉器,這種方式會構造大量無關的代碼,所以在C++11中引入了function和lambda運算式,減少代碼量,至于為什么要用函式物件:通過函式物件呼叫operator(),可以省略函式的呼叫開銷,比通過函式指標呼叫函式(不能夠inline行內呼叫)效率高,(見8.C++STL倒數第二段)
unique_ptr<int,function<void(int*)>> ptr1(new int[100],
[](int *p)->void{//使用lambda運算式
cout<<"call lambda release int[100]"<<endl;
delete[] p;
});
unique_ptr<FILE,function<void(FILE*)>> ptr2(fopen("data.txt","w"),
[](FILE* p)->void {
cout<<"call lambda release FILE*"<<endl;
fclose(p);
});
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/480178.html
標籤:其他
