首頁 > 軟體

C++智慧指標之shared_ptr詳解

2022-03-24 13:00:34

共用指標的初始化方式

1.裸指標直接初始化,但不能通過隱式轉換來構造

2.允許移動構造,也允許拷貝構造

3.通過make_shared構造

例:

#include <iostream>
#include <memory>
class Frame {};
int main()
{
  std::shared_ptr<Frame> f(new Frame());              // 裸指標直接初始化
  std::shared_ptr<Frame> f1 = new Frame();            // Error,explicit禁止隱式初始化
  std::shared_ptr<Frame> f2(f);                       // 拷貝建構函式
  std::shared_ptr<Frame> f3 = f;                      // 拷貝建構函式
  f2 = f;                                             // copy賦值運運算元過載
  std::cout << f3.use_count() << " " << f3.unique() << std::endl;
  std::shared_ptr<Frame> f4(std::move(new Frame()));        // 移動建構函式
  std::shared_ptr<Frame> f5 = std::move(new Frame());       // Error,explicit禁止隱式初始化
  std::shared_ptr<Frame> f6(std::move(f4));                 // 移動建構函式
  std::shared_ptr<Frame> f7 = std::move(f6);                // 移動建構函式
  std::cout << f7.use_count() << " " << f7.unique() << std::endl;
  std::shared_ptr<Frame[]> f8(new Frame[10]());             // Error,管理動態陣列時,需要指定刪除器
  std::shared_ptr<Frame> f9(new Frame[10](), std::default_delete<Frame[]>());
  auto f10 = std::make_shared<Frame>();               // std::make_shared來建立
  return 0;
}

注意:

1.儘量避免將一個裸指標傳遞給std::shared_ptr的建構函式,常用的替代手法是使用std::make_shared。如果必須將一個裸指標傳遞給shared_ptr的建構函式,就直接傳遞new運運算元的結果,而非傳遞一個裸指標變數。
2.不要將this指標返回給shared_ptr。當希望將this指標託管給shared_ptr時,類需要繼承自std::enable_shared_from_this,並且從shared_from_this()中獲得shared_ptr指標。
3.不要使用相同的原始指標作為實參來建立多個shared_ptr物件,具體原因見下面講的shared_ptr記憶體模型。可以使用拷貝構造或者直接使用過載運運算元=進行操作

例:

#include <iostream>
#include <memory>
class Frame {};
int main()
{
  Frame* f1 = new Frame();
  std::shared_ptr<Frame> f2(f1);
  std::shared_ptr<Frame> f3(f1);          // Error
  std::shared_ptr<Frame> f4(f2);
  auto f5 = f2;
  return 0;
}

常用成員函數

s.get():返回shared_ptr中儲存的裸指標;

s.reset(…):重置shared_ptr;

  • reset( )不帶引數時,若智慧指標s是唯一指向該物件的指標,則釋放,並置空。若智慧指標P不是唯一指向該物件的指標,則參照計數減少1,同時將P置空。
  • reset( )帶引數時,若智慧指標s是唯一指向物件的指標,則釋放並指向新的物件。若P不是唯一的指標,則只減少參照計數,並指向新的物件。如:
auto s = make_shared<int>(100);
s.reset(new int (200));

s.use_count():返回shared_ptr的強參照計數;

s.unique():若use_count()為1,返回true,否則返回false。

具體範例:

auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // 參照計數+1
auto pointer3 = pointer; // 參照計數+1
int *p = pointer.get(); // 這樣不會增加參照計數
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
pointer2.reset();
std::cout << "reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0, pointer2 已 reset
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
pointer3.reset();
std::cout << "reset pointer3:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 0, pointer3 已 reset

shared_ptr記憶體模型

由圖可以看出,shared_ptr包含了一個指向物件的指標和一個指向控制塊的指標。每一個由shared_ptr管理的物件都有一個控制塊,它除了包含強參照計數、弱參照計數之外,還包含了自定義刪除器的副本和分配器的副本以及其他附加資料。

控制塊的建立規則

  • std::make_shared總是建立一個控制塊;
  • 從具備所有權的指標出發構造一個std::shared_ptr時,會建立一個控制塊(如std::unique_ptr轉為shared_ptr時會建立控制塊,因為unique_ptr本身不使用控制塊,同時unique_ptr置空);
  • 當std::shared_ptr建構函式使用裸指標作為實參時,會建立一個控制塊。這意味從同一個裸指標出發來構造不止一個std::shared_ptr時會建立多重的控制塊,也意味著物件會被解構多次。如果想從一個己經擁有控制塊的物件出發建立一個std::shared_ptr,可以傳遞一個shared_ptr或weak_ptr而非裸指標作為建構函式的實參,或者直接使用過載運運算元=,這樣則不會建立新的控制塊。

因此,更好的解決方式是儘量避免使用裸指標作為共用指標的實參,而是使用make_shared,此外,make_shared相比直接new還具有以下好處

make_shared的優缺點

優點

  • 避免程式碼冗餘:建立智慧指標時,被建立物件的型別只需寫1次,而用new建立智慧指標時,需要寫2次;
  • 異常安全:make系列函數可編寫異常安全程式碼,改進了new的異常安全性;
  • 提升效能:編譯器有機會利用更簡潔的資料結構產生更小更快的程式碼。使用make_shared時會一次性進行記憶體分配,該記憶體單塊(single chunck)既儲存了T物件又儲存與其相關聯的控制塊。而直接使用new表示式,除了為T分配一次記憶體,還要為與其關聯的控制塊再進行一次記憶體分配。

make_shared與new方式記憶體分佈對比圖:

缺點

  • 所有的make系列函數都不允許自定義刪除器
  • make系列函數建立物件時,不能接受{}初始化列表(這是因為完美轉發的轉發函數是個模板函數,它利用模板型別進行推導。因此無法將{}推導為initializer_list)。換言之,make系列只能將圓括號內的形參完美轉發;
  • **自定義記憶體管理的類(如過載了operator new和operator delete),不建議使用make_shared來建立。**因為:過載operator new和operator delete時,往往用來分配和釋放該類精確尺寸(sizeof(T))的記憶體塊;而make_shared建立的shared_ptr,是一個自定義了分配器(std::allocate_shared)和刪除器的智慧指標,由allocate_shared分配的記憶體大小也不等於上述的尺寸,而是在此基礎上加上控制塊的大小;
  • 物件的記憶體可能無法及時回收。因為:make_shared只分配一次記憶體,減少了記憶體分配的開銷,使得控制塊和託管物件在同一記憶體塊上分配。而控制塊是由shared_ptr和weak_ptr共用的,因此兩者共同管理著這個記憶體塊(託管物件+控制塊)。當強參照計數為0時,託管物件被解構(即解構函式被呼叫),但記憶體塊並未被回收,只有等到最後一個weak_ptr離開作用域時,弱參照也減為0才會釋放這塊記憶體塊。原本強參照減為0時就可以釋放的記憶體, 現在變為了強參照和弱參照都減為0時才能釋放, 意外的延遲了記憶體釋放的時間。這對於記憶體要求高的場景來說, 是一個需要注意的問題。

參照計數

  • shared_ptr中的參照計數直接關係到何時是否進行物件的解構,因此它的變動尤其重要。
  • shared_ptr的**建構函式會使該參照計數遞增,而解構函式會使該計數遞減。**但移動構造表示從一個己有的shared_ptr移動構造到一個新的shared_ptr。這意味著一旦新的shared_ptr產生後,原有的shared_ptr會被置空,其結果是參照計數沒有變化;
  • 拷貝賦值操作同時執行兩種操作(如sp1和sp2是指向不同物件的shared_ptr,則執行sp1=sp2時,將修改sp1使得其指向sp2所指的物件。而最初sp1所指向的物件的參照計數遞減,同時sp2所指向的物件參照計數遞增);
  • reset函數,如果不帶引數時,則參照計數減1。如果帶引數時,如sp.reset( p )則sp原來指向的物件參照計數減1,同時sp指向新的物件( p );
  • 如果實施一次遞減後最後的參照計數變成0,即不再有shared_ptr指向該物件,則會被shared_ptr解構掉;
  • 參照計數的遞增和遞減是原子操作,即允許不同執行緒並行改變參照計數。

比較運運算元

所有比較運運算元都會呼叫共用指標內部封裝的原始指標的比較運運算元;支援==、!=、<、<=、>、>=;同型別的共用指標才能使用比較運運算元

shared_ptr<int> sp_n1 = make_shared<int>(1);
shared_ptr<int> sp_n2 = make_shared<int>(2);
shared_ptr<int> sp_nu;
shared_ptr<double> sp_d1 = 
    make_shared<double>(1);
bool bN1LtN2 = sp_n1 < sp_n2;  //true
bool bN1GtNu = sp_n1 > sp_nu;  //true
bool bNuEqNu = sp_nu == sp_nu; //true
bool bN2GtD1 = sp_d1 < sp_n2;  //編譯錯誤

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!    


IT145.com E-mail:sddin#qq.com