文章首發
【重學C++】03 | 手擼C++智能指標實戰教程
前言
大家好,今天是【重學C++】的第三講,書接上回,第二講《02 脫離指標陷阱:深入淺出 C++ 智能指標》介紹了C++智能指標的一些使用方法和基本原理,今天,我們自己動手,從0到1實作一下自己的unique_ptr
和shared_ptr
,
回顧
智能指標的基本原理是基于RAII設計理論,自動回收記憶體資源,從根本上避免記憶體泄漏,在第一講《01 C++ 如何進行記憶體資源管理?》介紹RAII的時候,就已經給了一個用于封裝int
型別指標,實作自動回收資源的代碼實體:
class AutoIntPtr {
public:
AutoIntPtr(int* p = nullptr) : ptr(p) {}
~AutoIntPtr() { delete ptr; }
int& operator*() const { return *ptr; }
int* operator->() const { return ptr; }
private:
int* ptr;
};
我們從這個示例出發,一步步完善我們自己的智能指標,
模版化
這個類有個明顯的問題:只能適用于int類指標,所以我們第一步要做的,就是把它改造成一個類模版,讓這個類適用于任何型別的指標資源,
code show time
template <typename T>
class smart_ptr {
public:
explicit smart_ptr(T* ptr = nullptr): ptr_(ptr) {}
~smart_ptr() {
delete ptr_;
}
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
private:
T* ptr_;
}
我給我們的智能指標類用了一個更抽象,更切合的類名:smart_ptr
,
和AutoIntPtr
相比,我們把smart_ptr
設計成一個類模版,原來代碼中的int
改成模版引數T
,非常簡單,使用時也只要把AutoIntPtr(new int(9))
改成smart_ptr<int>(new int(9))
即可,
另外,有一點值得注意,smart_ptr
的建構式使用了explicit
, explicit
關鍵字主要用于防止隱式的型別轉換,代碼中,如果原生指標隱式地轉換為智能指標型別可能會導致一些潛在的問題,至于會有什么問題,你那聰明的小腦瓜看完下面的代碼肯定能理解了:
void foo(smart_ptr<int> int_ptr) {
// ...
}
int main() {
int* raw_ptr = new int(42);
foo(raw_ptr); // 隱式轉換為 smart_ptr<int>
std::cout << *raw_ptr << std::endl; // error: raw_ptr已經被回收了
// ...
}
假設我們沒有為smart_ptr
建構式加上explicit
,原生指標raw_ptr
在傳給foo
函式后,會被隱形轉換為smart_ptr<int>
, foo
函式呼叫結束后,棲構入參的smart_ptr<int>
時會把raw_ptr
給回收掉了,所以后續對raw_ptr
的呼叫都會失敗,
拷貝還是移動?
當前我們沒有為smart_ptr
自定義拷貝建構式/移動建構式,C++會為smart_ptr
生成默認的拷貝/移動建構式,默認的拷貝/移動建構式邏輯很簡單:把每個成員變數拷貝/移動到目標物件中,
按當前smart_ptr
的實作,我們假設有以下代碼:
smart_ptr<int> ptr1{new int(10)};
smart_ptr<int> ptr2 = ptr1;
這段代碼在編譯時不會出錯,問題在運行時才會暴露出來:第二行將ptr1
管理的指標復制給了ptr2
,所以會重復釋放記憶體,導致程式奔潰,
為了避免同一塊記憶體被重復釋放,解決辦法也很簡單:
- 獨占資源所有權,每時每刻一個記憶體物件(資源)只能有一個
smart_ptr
占有它, - 一個記憶體物件(資源)只有在最后一個擁有它的
smart_ptr
析構時才會進行資源回收,
獨占所有權 - unique_smart_ptr
獨占資源的所有權,并不是指禁用掉smart_ptr
的拷貝/移動函式(當然這也是一種簡單的避免重復釋放記憶體的方法),而是smart_ptr
在拷貝時,代表資源物件的指標不是復制到另外一個smart_ptr
,而是"移動"到新smart_ptr
,移動后,原來的smart_ptr.ptr_
== nullptr, 這樣就完成了資源所有權的轉移,
這也是C++ unique_ptr
的基本行為,我們在這里先把它命名為unique_smart_ptr
,代碼完整實作如下:
template <typename T>
class unique_smart_ptr {
public:
explicit unique_smart_ptr(T* ptr = nullptr): ptr_(ptr) {}
~unique_smart_ptr() {
delete ptr_;
}
// 1. 自定義移動建構式
unique_smart_ptr(unique_smart_ptr&& other) {
// 1.1 把other.ptr_ 賦值到this->ptr_
ptr_ = other.ptr_;
// 1.2 把other.ptr_指為nullptr,other不再擁有資源指標
other.ptr_ = nullptr;
}
// 2. 自定義賦值行為
unique_smart_ptr& operator = (unique_smart_ptr rhs) {
// 2.1 交換rhs.ptr_和this->ptr_
std::swap(rhs.ptr_, this->ptr_);
return *this;
}
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
private:
T* ptr_;
};
自定義移動建構式,在移動建構式中,我們先是接管了other.ptr_
指向的資源物件,然后把other
的ptr_
置為nullptr,這樣在other
析構時就不會錯誤釋放資源記憶體,
同時,根據C++的規則,手動提供移動建構式后,就會自動禁用拷貝建構式,也就是我們能得到以下效果:
unique_smart_ptr<int> ptr1{new int(10)};
unique_smart_ptr<int> ptr2 = ptr1; // error
unique_smart_ptr<int> ptr3 = std::move(ptr1); // ok
unique_smart_ptr<int> ptr4{ptr1} // error
unique_smart_ptr<int> ptr5{std::move(ptr1)} // ok
自定義賦值函式,在賦值函式中,我們使用std::swap
交換了 rhs.ptr_
和this->ptr_
,注意,這里不能簡單的將rhs.ptr_
設定為nullptr,因為this->ptr_
可能有指向一個堆物件,該物件需要轉給rhs
,在賦值函式呼叫結束,rhs
析構時順便釋放掉,避免記憶體泄漏,
注意賦值函式的入參rhs
的型別是unique_smart_ptr
而不是unique_smart_ptr&&
,這樣創建rhs
使用移動建構式還是拷貝建構式完全取決于unique_smart_ptr
的定義,因為unique_smart_ptr
當前只保留了移動建構式,所以rhs
是通過移動建構式創建的,
多個智能指標共享物件 - shared_smart_ptr
學過第二講的shared_ptr
, 我們知道它是利用計數參考的方式,實作了多個智能指標共享同一個物件,當最后一個持有物件的智能指標析構時,計數器減為0,這個時候才會回收資源物件,
我們先給出shared_smart_ptr
的類定義
template <typename T>
class shared_smart_ptr {
public:
// 建構式
explicit shared_smart_ptr(T* ptr = nullptr)
// 解構式
~shared_smart_ptr()
// 移動建構式
shared_smart_ptr(shared_smart_ptr&& other)
// 拷貝建構式
shared_smart_ptr(const shared_smart_ptr& other)
// 賦值函式
shared_smart_ptr& operator = (shared_smart_ptr rhs)
// 回傳當前參考次數
int use_count() const { return *count_; }
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
private:
T* ptr_;
int* count_;
}
暫時不考慮多執行緒并發安全的問題,我們簡單在堆上創建一個int型別的計數器count_
,下面詳細展開各個函式的實作,
為了避免對
count_
的重復洗掉,我們保持:只有當ptr_ != nullptr
時,才對count_
進行賦值,
建構式
同樣的,使用explicit
避免隱式轉換,除了賦值ptr_
, 還需要在堆上創建一個計數器,
explicit shared_smart_ptr(T* ptr = nullptr){
ptr_ = ptr;
if (ptr_) {
count_ = new int(1);
}
}
解構式
在解構式中,需要根據計數器的參考數判斷是否需要回收物件,
~shared_smart_ptr() {
// ptr_為nullptr,不需要做任何處理
if (ptr_) {
return;
}
// 計數器減一
--(*count_);
// 計數器減為0,回收物件
if (*count_ == 0) {
delete ptr_;
delete count_;
return;
}
}
移動建構式
添加對count_
的處理
shared_smart_ptr(shared_smart_ptr&& other) {
ptr_ = other.ptr_;
count_ = other.count_;
other.ptr_ = nullptr;
other.count_ = nullptr;
}
賦值建構式
添加交換count_
shared_smart_ptr& operator = (shared_smart_ptr rhs) {
std::swap(rhs.ptr_, this->ptr_);
std::swap(rhs.count_, this->count_);
return *this;
}
拷貝建構式
對于shared_smart_ptr
,我們需要手動支持拷貝建構式,主要處理邏輯是賦值ptr_
和增加計數器的參考數,
shared_smart_ptr(const shared_smart_ptr& other) {
ptr_ = other.ptr_;
count_ = other.count_;
if (ptr_) {
(*count_)++;
}
}
這樣,我們就實作了一個自己的共享智能指標,貼一下完整代碼
template <typename T>
class shared_smart_ptr {
public:
explicit shared_smart_ptr(T* ptr = nullptr){
ptr_ = ptr;
if (ptr_) {
count_ = new int(1);
}
}
~shared_smart_ptr() {
// ptr_為nullptr,不需要做任何處理
if (ptr_ == nullptr) {
return;
}
// 計數器減一
--(*count_);
// 計數器減為0,回收物件
if (*count_ == 0) {
delete ptr_;
delete count_;
}
}
shared_smart_ptr(shared_smart_ptr&& other) {
ptr_ = other.ptr_;
count_ = other.count_;
other.ptr_ = nullptr;
other.count_ = nullptr;
}
shared_smart_ptr(const shared_smart_ptr& other) {
ptr_ = other.ptr_;
count_ = other.count_;
if (ptr_) {
(*count_)++;
}
}
shared_smart_ptr& operator = (shared_smart_ptr rhs) {
std::swap(rhs.ptr_, this->ptr_);
std::swap(rhs.count_, this->count_);
return *this;
}
int use_count() const { return *count_; };
T& operator*() const { return *ptr_; };
T* operator->() const { return ptr_; };
private:
T* ptr_;
int* count_;
};
使用下面代碼進行驗證:
int main(int argc, const char** argv) {
shared_smart_ptr<int> ptr1(new int(1));
std::cout << "[初始化ptr1] use count of ptr1: " << ptr1.use_count() << std::endl;
{
// 賦值使用拷貝建構式
shared_smart_ptr<int> ptr2 = ptr1;
std::cout << "[使用拷貝建構式將ptr1賦值給ptr2] use count of ptr1: " << ptr1.use_count() << std::endl;
// 賦值使用移動建構式
shared_smart_ptr<int> ptr3 = std::move(ptr2);
std::cout << "[使用移動建構式將ptr2賦值給ptr3] use count of ptr1: " << ptr1.use_count() << std::endl;
}
std::cout << "[ptr2和ptr3析構后] use count of ptr1: " << ptr1.use_count() << std::endl;
}
運行結果:
[初始化ptr1] use count of ptr1: 1
[使用拷貝建構式將ptr1賦值給ptr2] use count of ptr1: 2
[使用移動建構式將ptr2賦值給ptr3] use count of ptr1: 2
[ptr2和ptr3析構后] use count of ptr1: 1
總結
這一講我們從AutoIntPtr
出發,先是將類進行模版化,使其能夠管理任何型別的指標物件,并給該類起了一個更抽象、更貼切的名稱——smart_ptr
,
接著圍繞著「如何正確釋放資源物件指標」的問題,一步步手擼了兩個智能指標 ——unique_smart_ptr
和shared_smart_ptr
,相信大家現在對智能指標有一個較為深入的理解了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/553032.html
標籤:C++
上一篇:獻給轉java的c#和java程式員的資料庫orm框架
下一篇:返回列表