前言
之前學習muduo網路庫的時候,看到作者陳碩用到了enable_shared_from_this和shared_from_this,一直對此概念是一個模糊的認識,隱約記著這個機制是在計數器智能指標傳遞時才會用到的,今天對該機制進行梳理總結一下吧,
如果不熟悉C++帶參考計數的智能指標shared_ptr和weak_ptr,可參考這篇文章:??深入掌握智能指標
這篇文章主要介紹C++11提供的智能指標相關的enable_shared_from_this和shared_from_this機制,
問題代碼
我們先給出兩個智能指標的應用場景代碼,這些代碼都有問題,仔細思考下問題原因,
代碼清單1
#include <iostream>
#include <memory>
using namespace std;
// 智能指標測驗類
class A {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int *m_ptr;
};
int main() {
A *p = new A(); // 裸指標指向堆上的物件
shared_ptr<A> ptr1(p); // 用shared_ptr智能指標管理指標p指向的物件
shared_ptr<A> ptr2(p); // 用shared_ptr智能指標管理指標p指向的物件
// 下面兩次列印都是1,因此同一個new A()被析構兩次,邏輯錯誤
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代碼列印結果如下:
A()
1
1
~A()
~A()
free(): double free detected in tcache 2
Aborted (core dumped)
main函式中,雖然用了兩個智能指標shared_ptr,但是它們管理的都是同一個資源,資源的參考計數應該是2,為什么列印出來是1呢?導致出main函式把A物件析構了兩次,不正確!如果你有這樣的疑問,說明對于shared_ptr的底層原理還沒有完全搞清楚,
代碼清單2
#include <iostream>
using namespace std;
// 智能指標測驗類
class A {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
}
// A類提供了一個成員方法,回傳指向自身物件的shared_ptr智能指標,
shared_ptr<A> getSharedPtr() {
/*注意:不能直接回傳this,在多執行緒環境下,根本無法獲知this指標指向
的物件的生存狀態,通過shared_ptr和weak_ptr可以解決多執行緒訪問共享
物件的執行緒安全問題,參考我的另一篇介紹智能指標的博客*/
return shared_ptr<A>(this);
}
private:
int *m_ptr;
};
int main() {
shared_ptr<A> ptr1(new A());
shared_ptr<A> ptr2 = ptr1->getSharedPtr();
/* 按原先的想法,上面兩個智能指標管理的是同一個A物件資源,但是這里列印都是1
導致出main函式A物件析構兩次,析構邏輯有問題*/
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代碼運行結果列印如下:
A()
1
1
~A()
~A()
free(): double free detected in tcache 2
Aborted (core dumped)
代碼同樣有錯誤,A物件被析構了兩次,而且看似兩個shared_ptr指向了同一個A物件資源,但是資源計數并沒有記錄成2,還是1,不正確,
shared_ptr原理分析
如果你能夠理解上面代碼的問題所在,那么直接跳到下一節看上面錯誤代碼的解決方案;如果不明白問題的所在,通過下面的原始碼介紹,仔細理解shared_ptr的實作原理,
原始碼上shared_ptr的定義如下:
template<class _Ty>
class shared_ptr
: public _Ptr_base<_Ty>
shared_ptr是從_Ptr_base繼承而來的,作為派生類,shared_ptr本身沒有提供任何成員變數,但是它從基類_Ptr_base繼承來了如下成員變數(只羅列部分原始碼):
template<class _Ty>
class _Ptr_base
{ // base class for shared_ptr and weak_ptr
protected:
void _Decref()
{ // decrement reference count
if (_Rep)
{
_Rep->_Decref();
}
}
void _Decwref()
{ // decrement weak reference count
if (_Rep)
{
_Rep->_Decwref();
}
}
private:
// _Ptr_base的兩個成員變數,這里只羅列了_Ptr_base的部分代碼
element_type * _Ptr{nullptr}; // 指向資源的指標
_Ref_count_base * _Rep{nullptr}; // 指向資源參考計數的指標
};
_Ref_count_base記錄資源的類是怎么定義的呢?如下(只羅列部分原始碼):
class __declspec(novtable) _Ref_count_base
{ // common code for reference counting
private:
/**
* _Uses記錄了資源的參考計數,也就是參考資源的shared_ptr的個數;
* _Weaks記錄了weak_ptr的個數,相當于資源觀察者的個數,都是定義成基于CAS操作的原子型別,增減參考計數時時執行緒安全的操作
**/
_Atomic_counter_t _Uses;
_Atomic_counter_t _Weaks;
}
也就是說,當我們定義一個shared_ptr<int> ptr(new int)的智能指標物件時,該智能指標物件本身的記憶體是8個位元組,如下圖所示:
那么把智能指標管理的外部資源以及參考計數資源都畫出來的話,就是如下圖的展示:
當你做這樣的代碼操作時:
shared_ptr<int> ptr1(new int);
shared_ptr<int> ptr2(ptr1);
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
這段代碼沒有任何問題,ptr1和ptr2管理了同一個資源,參考計數列印出來的都是2,出函式作用域依次析構,最終new int資源只釋放一次,邏輯正確!這是因為shared_ptr ptr2(ptr1)呼叫了shared_ptr的拷貝建構式(原始碼可以自己查看下),只是做了資源的參考計數的改變,沒有額外分配其它資源,如下圖所示:
注意:兩個shared_ptr物件參考的是同一個參考計數物件_Ref_count_base,依次析構的時候,最終資源new int只釋放一次,正確
但是當你做如下代碼操作時:
int *p = new int;
shared_ptr<int> ptr1(p);
shared_ptr<int> ptr2(p);
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
這段代碼就有問題了,因為shared_ptr<int> ptr1(p)和shared_ptr<int> ptr2(p)都呼叫了shared_ptr的建構式,在它的建構式中,都重新開辟了參考計數的資源,導致ptr1和ptr2都記錄了一次new int的參考計數,都是1,析構的時候它倆都去釋放記憶體資源,導致釋放邏輯錯誤,如下圖所示:

注意:兩個shared_ptr物件都開辟了自己的參考計數物件_Ref_count_base,都記錄new int資源的參考計數為1,析構的時候參考計數減到0,都認為自己該釋放new int資源,錯誤!
上面兩個代碼段,分別是shared_ptr的建構式和拷貝建構式做的事情,導致雖然都是指向同一個new int資源,但是對于參考計數物件的管理方式,這兩個函式是不一樣的,建構式是新分配參考計數物件,拷貝建構式只做參考計數增減,
相信說到這里,大家知道最開始的兩個代碼清單上的代碼為什么出錯了吧,因為每次呼叫的都是shared_ptr的建構式,雖然大家管理的資源都是一樣的,_Ptr都是指向同一個堆記憶體,但是_Rep卻指向了不同的參考計數物件,并且都記錄參考計數是1,出作用域都去析構,使得同一塊記憶體被析構多次,導致問題發生!
問題修改
代碼清單1修改
那么清單1的代碼修改很簡單,就是在產生同一資源的多個shared_ptr的時候,通過拷貝建構式或者賦值operator=函式進行,不要重新構造,避免產生多個參考計數物件,代碼修改如下:
int main() {
A *p = new A(); // 裸指標指向堆上的物件
shared_ptr<A> ptr1(p); // 用shared_ptr智能指標管理指標p指向的物件
shared_ptr<A> ptr2(ptr1); // 用ptr1拷貝構造ptr2
// 下面兩次列印都是2,最終隨著ptr1和ptr2析構,資源只釋放一次,正確!
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代碼清單2修改 enable_shared_from_this和shared_from_this
那么清單2代碼怎么修改呢?注意我們有時候想在類里面提供一些方法,回傳當前物件的一個shared_ptr強智能指標,做引數傳遞使用(多執行緒編程中經常會用到),
首先肯定不能像上面代碼清單2那樣寫return shared_ptr<A> (this),這會呼叫shared_ptr智能指標的建構式,對this指標指向的物件,又建立了一份參考計數物件,加上main函式中的shared_ptr<A> ptr1(new A());已經對這個A物件建立的參考計數物件,又成了兩個參考計數物件,對同一個資源都記錄了參考計數,為1,最終兩次析構物件釋放記憶體,錯誤!
那如果一個類要提供一個函式介面,回傳一個指向當前物件的shared_ptr智能指標怎么辦?方法就是繼承enable_shared_from_this類,然后通過呼叫從基類繼承來的shared_from_this()方法回傳指向同一個資源物件的智能指標shared_ptr,
修改如下:
#include <iostream>
#include <memory>
using namespace std;
// 智能指標測驗類,繼承enable_shared_from_this類
class A : public enable_shared_from_this<A> {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
}
// A類提供了一個成員方法,回傳指向自身物件的shared_ptr智能指標
shared_ptr<A> getSharedPtr() {
/*通過呼叫基類的shared_from_this方法得到一個指向當前物件的智能指標*/
return shared_from_this();
}
private:
int *m_ptr;
};
一個類繼承enable_shared_from_this會怎么樣?看看enable_shared_from_this基類的成員變數有什么,如下:
template<class _Ty>
class enable_shared_from_this
{ // provide member functions that create shared_ptr to this
public:
using _Esft_type = enable_shared_from_this;
_NODISCARD shared_ptr<_Ty> shared_from_this()
{ // return shared_ptr
return (shared_ptr<_Ty>(_Wptr));
}
// 成員變數是一個指向資源的弱智能指標
mutable weak_ptr<_Ty> _Wptr;
};
也就是說,如果一個類繼承了enable_shared_from_this,那么它產生的物件就會從基類enable_shared_from_this繼承一個成員變數_Wptr,當定義第一個智能指標物件的時候shared_ptr<A> ptr1(new A()),呼叫shared_ptr的普通建構式,就會初始化A物件的成員變數_Wptr,作為觀察A物件資源的一個弱智能指標觀察者(在shared_ptr的建構式中實作,有興趣可以自己除錯跟蹤原始碼實作),
然后代碼如下呼叫shared_ptr<A> ptr2 = ptr1->getSharedPtr(),getSharedPtr函式內部呼叫shared_from_this()函式回傳指向該物件的智能指標,這個函式怎么實作的呢,看原始碼:
shared_ptr<_Ty> shared_from_this()
{ // return shared_ptr
return (shared_ptr<_Ty>(_Wptr));
}
shared_ptr<_Ty>(_Wptr),說明通過當前A物件的成員變數_Wptr構造一個shared_ptr出來,看看shared_ptr相應的建構式:
shared_ptr(const weak_ptr<_Ty2>& _Other)
{ // construct shared_ptr object that owns resource *_Other
if (!this->_Construct_from_weak(_Other)) // 從弱智能指標提升一個強智能指標
{
_THROW(bad_weak_ptr{});
}
}
接著看上面呼叫的_Construct_from_weak方法的實作如下:
template<class _Ty2>
bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other)
{ // implement shared_ptr's ctor from weak_ptr, and weak_ptr::lock()
// if通過判斷資源的參考計數是否還在,判定物件的存活狀態,物件存活,提升成功;
// 物件析構,提升失敗!之前的博客內容講過這些知識,可以去參考!
if (_Other._Rep && _Other._Rep->_Incref_nz())
{
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
return (true);
}
return (false);
}
綜上所說,所有程序都沒有再使用shared_ptr的普通建構式,沒有在產生額外的參考計數物件,不會存在把一個記憶體資源,進行多次計數的程序;更關鍵的是,通過weak_ptr到shared_ptr的提升,還可以在多執行緒環境中判斷物件是否存活或者已經析構釋放,在多執行緒環境中是很安全的,通過this裸指標進行構造shared_ptr,不僅僅資源會多次釋放,而且在多執行緒環境中也不確定this指向的物件是否還存活,
最終代碼清單2修改如下:
#include <iostream>
#include <memory>
using namespace std;
// 智能指標測驗類,繼承enable_shared_from_this類
class A : public enable_shared_from_this<A> {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
}
// A類提供了一個成員方法,回傳指向自身物件的shared_ptr智能指標
shared_ptr<A> getSharedPtr() {
/*通過呼叫基類的shared_from_this方法得到一個指向當前物件的智能指標*/
return shared_from_this();
}
private:
int *m_ptr;
};
int main() {
shared_ptr<A> ptr1(new A());
shared_ptr<A> ptr2 = ptr1->getSharedPtr();
// 參考計數列印為2
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代碼列印結果如下:
A()
2
2
~A()
列印完全正確,A物件構造一次,析構一次,參考計數為2,
總結
以上就是對enable_shared_from_this和shared_from_this機制的介紹,這東西主要解決了該問題:當回傳某物件時,由于智能指標呼叫常規的建構式導致參考計數類的多次構造,從而導致在釋放記憶體時,多個智能指標對同一塊記憶體進行多次釋放,出現Core dump,使用該機制則可回傳指向某物件的智能指標,這樣就呼叫的是智能指標的拷貝建構式而非常規的建構式,使得參考計數類不會被多次構造,避免出現同一記憶體多次釋放的情況,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/511035.html
標籤:其他
上一篇:位運算及進制轉換
下一篇:HTTP協議有必要學習嗎?
