主頁 > 後端開發 > 【重學C++】03 | 手擼C++智能指標實戰教程

【重學C++】03 | 手擼C++智能指標實戰教程

2023-05-23 07:32:52 後端開發

文章首發

【重學C++】03 | 手擼C++智能指標實戰教程

前言

大家好,今天是【重學C++】的第三講,書接上回,第二講《02 脫離指標陷阱:深入淺出 C++ 智能指標》介紹了C++智能指標的一些使用方法和基本原理,今天,我們自己動手,從0到1實作一下自己的unique_ptrshared_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的建構式使用了explicitexplicit關鍵字主要用于防止隱式的型別轉換,代碼中,如果原生指標隱式地轉換為智能指標型別可能會導致一些潛在的問題,至于會有什么問題,你那聰明的小腦瓜看完下面的代碼肯定能理解了:

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,所以會重復釋放記憶體,導致程式奔潰,

為了避免同一塊記憶體被重復釋放,解決辦法也很簡單:

  1. 獨占資源所有權,每時每刻一個記憶體物件(資源)只能有一個smart_ptr占有它,
  2. 一個記憶體物件(資源)只有在最后一個擁有它的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_指向的資源物件,然后把otherptr_置為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是通過移動建構式創建的,
Pasted image 20230426083308

多個智能指標共享物件 - shared_smart_ptr

學過第二講的shared_ptr, 我們知道它是利用計數參考的方式,實作了多個智能指標共享同一個物件,當最后一個持有物件的智能指標析構時,計數器減為0,這個時候才會回收資源物件,
image.png

我們先給出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_ptrshared_smart_ptr,相信大家現在對智能指標有一個較為深入的理解了,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/553032.html

標籤:C++

上一篇:獻給轉java的c#和java程式員的資料庫orm框架

下一篇:返回列表

標籤雲
其他(159443) Python(38156) JavaScript(25441) Java(18079) C(15229) 區塊鏈(8267) C#(7972) AI(7469) 爪哇(7425) MySQL(7204) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5340) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4574) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2433) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1975) 功能(1967) Web開發(1951) HtmlCss(1940) C++(1919) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1878) .NETCore(1861) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • 【重學C++】03 | 手擼C++智能指標實戰教程

    ## 文章首發 [【重學C++】03 | 手擼C++智能指標實戰教程](https://mp.weixin.qq.com/s/B85A_AFIAeOlfLzXOXydAw) ## 前言 大家好,今天是【重學C++】的第三講,書接上回,第二講《[02 脫離指標陷阱:深入淺出 C++ 智能指標](htt ......

    uj5u.com 2023-05-23 07:32:52 more
  • 獻給轉java的c#和java程式員的資料庫orm框架

    # 獻給轉java的c#和java程式員的資料庫orm框架 一個好的程式員不應被語言所束縛,正如我現在開源java的orm框架一樣,如果您是一位轉java的c#程式員,那么這個框架可以帶給你起碼沒有那么差的業務撰寫和強型別體驗。如果您是一位java程式員,那么該框架可以提供比`Mybatis-Plu ......

    uj5u.com 2023-05-22 08:27:33 more
  • SICP:惰性求值、流和尾遞回(Python實作)

    在上一篇博客中,我們介紹了用Python對來實作一個Scheme求值器。然而,我們跳過了部分特殊形式(special forms)和基本程序(primitive procedures)實作的介紹,如特殊形式中的delay、cons-stream,基本程序中的force、streawn-car、str... ......

    uj5u.com 2023-05-22 07:35:15 more
  • 使用 Async Rust 構建簡單的 P2P 節點

    # 使用 Async Rust 構建簡單的 P2P 節點 ### P2P 簡介 - P2P:peer-to-peer - P2P 是一種網路技術,可以在不同的計算機之間共享各種計算資源,如 CPU、網路帶寬和存盤。 - P2P 是當今用戶在線共享檔案(如音樂、影像和其他數字媒體)的一種非常常用的方法 ......

    uj5u.com 2023-05-22 07:33:10 more
  • 用go設計開發一個自己的輕量級登錄庫/框架吧(拓展篇)

    用go設計開發一個自己的輕量級登錄庫/框架吧(拓展篇),給自己的庫/框架拓展一下吧,主庫:https://github.com/weloe/token-go ......

    uj5u.com 2023-05-22 07:30:30 more
  • Java 網路編程 —— 實作非阻塞式的客戶端

    ## 創建阻塞的 EchoClient 客戶程式一般不需要同時建立與服務器的多個連接,因此用一個執行緒,按照阻塞模式運行就能滿足需求 ```java public class EchoClient { private SocketChannel socketChannel = null; public ......

    uj5u.com 2023-05-21 07:32:01 more
  • 基于Django的簡易博客系統教程

    ## 1. 安裝Django 在命令列中輸入以下命令安裝Django ```shell pip install django ``` ## 2. 創建Django專案 在命令列中輸入以下命令創建一個名為myblog的Django專案 ```shell django-admin startprojec ......

    uj5u.com 2023-05-21 07:31:58 more
  • java集合框架

    # java集合框架 - 概念:物件的容器,定義了對多個物件進行操作的常用方法。可以實作陣列的功能。 - 和陣列的區別: 1. 陣列長度固定,集合長度不固定 2. 陣列可以存盤基本資料型別和參考資料型別,集合只能存盤參考資料型別(存盤基本資料型別自動裝箱) ## Collection類 所有集合類的 ......

    uj5u.com 2023-05-21 07:31:44 more
  • 【重學C++】01| C++ 如何進行記憶體資源管理?

    ## 文章首發 [【重學C++】01| C++ 如何進行記憶體資源管理?](https://mp.weixin.qq.com/s/ZhRhN07wjypnkWXcu_Lz3g) ## 前言 大家好,我是只講技術干貨的會玩code,今天是【重學C++】的第一講,我們來學習下C++的記憶體管理。 與java ......

    uj5u.com 2023-05-21 07:31:31 more
  • Python潮流周刊#2:Rust 讓 Python 再次偉大

    這里記錄每周值得分享的 Python 及通用技術內容,部分為英文,已在小標題注明。(本期標題取自其中一則分享,不代表全部內容都是該主題,特此宣告。) ## 文章&教程 1、[Python修飾器的函式式編程](http://coolshell.cn/articles/11265.html "Pytho ......

    uj5u.com 2023-05-21 07:25:15 more