現代C++(Modern C++)基本用法實踐:五、智慧指標

2023-07-14 18:00:57

概述

c++效率較高的一個原因是我們可以自己客製化策略手動申請和釋放記憶體,當然,也伴隨著開發效率降低和記憶體漏失的風險。為了減少手動管理記憶體帶來的困擾,c++提出了智慧指標,可以幫助我們進行記憶體管理,有三種:

  1. std::unique_ptr 是一種獨佔所有權的智慧指標,它不允許多個指標指向同一個物件。std::unique_ptr 是一種輕量級的智慧指標,不需要額外的開銷,因此在你不需要共用所有權的情況下,應優先使用 std::unique_ptr

  2. std::shared_ptr 是一種可以共用所有權的智慧指標。多個 std::shared_ptr 可以指向同一個物件,該物件只有在最後一個指向它的 std::shared_ptr 被銷燬時才會被刪除。std::shared_ptr 使用參照計數來跟蹤有多少個智慧指標指向同一個物件。

  3. std::weak_ptr 是一種弱參照智慧指標,它可以指向 std::shared_ptr 所管理的物件,但它不會增加該物件的參照計數。這對於解決 std::shared_ptr 的迴圈參照問題很有用。

智慧指標的型別決定了何時銷燬和釋放記憶體。當智慧指標的生命週期結束時,它會自動清理其所有權的記憶體。這就避免了記憶體漏失,使程式碼更安全、更健壯。
但是智慧指標的使用,也要注意一些問題:

  • 智慧指標並不能自動解決迴圈參照的問題,需要手動在合適的場合使用std::weak_ptr弱指標
  • 智慧指標的使用是「傳染性」的,如果使用智慧指標,要考慮好整個邏輯鏈條都使用智慧指標而不是原生的指標
  • 從實際專案使用上看,一些比較上層的模組使用智慧指標,更加方便,而在底層模組更多的還是使用原生的指標,我想大概因為智慧指標也是會帶來一些開銷而且可能和一些記憶體管理策略衝突(如某些優化記憶體策略可能不使用指標,而使用控制程式碼來指向物件)

智慧指標是一個物件,實現是非入侵式的。關於他們的大概的原理:

  • std::unique_ptr 它包含一個原生指標,並在其解構函式中刪除這個指標。std::unique_ptr還過載了->*運運算元,所以可以像使用原生指標一樣使用std::unique_ptr
  • std::shared_ptr 的實現則相對複雜一些。除了儲存原生指標,std::shared_ptr還需要儲存一個參照計數。每次建立一個新的std::shared_ptr或者呼叫std::shared_ptr的拷貝建構函式或賦值運運算元時,參照計數就會增加。每次銷燬一個std::shared_ptr時,參照計數就會減少。只有當參照計數降到0時,才會刪除原生指標。類似的還要處理若參照。此外,參照計數還要保證執行緒安全。

用法舉例

參考測試專案程式碼ModernCppTest/modrenc_smart_pointer.cpp
主要包含:

  • 共用指標用法&宣告方式
  • 獨佔指標用法&宣告方式
  • 弱指標的用法
#include "ModernCppTestHeader.h"
#include <memory>

#define LOG_UNIQUE_PTR_VALID(ptr) if(ptr) LOG(#ptr << " unique_ptr valid"); else LOG(#ptr << " unique_ptr invalid")
#define LOG_WEAK_PTR_IF_VALID(ptr, exp) if(auto tp = p2.lock()) exp else LOG(#ptr << " weak_ptr invalid")

namespace n_smart_pointer {
	class Obj {
	public:
		Obj(int ID) : ID(ID) { LOG("Obj addr " << this << " ID " << ID << " Create"); }
		~Obj() { LOG("Obj addr " << this << " ID " << ID << " Release"); }

		int ID;
	};

	class Item {};
}

using LocaObj = n_smart_pointer::Obj;

void smart_pointer_test()
{
	LOG_FUNC();


	LOG_TAG("std::shared_ptr 共用指標");
	{
		{
			std::shared_ptr<LocaObj> p1 = std::make_shared<LocaObj>(1);
			{
				std::shared_ptr<LocaObj> p2 = p1;
				LOG_VAR_DESC(p1->ID, " p1->ID");
				LOG_VAR_DESC(p2->ID, " p2->ID");
				LOG("p2 離開作用域");
			}
			LOG("p1 離開作用域");
		}
	}


	LOG_TAG("std::shared_ptr 共用指標的宣告方式");
	{
		std::shared_ptr<LocaObj> p1(new LocaObj(1));
		std::shared_ptr<LocaObj> p2 = std::make_shared<LocaObj>(2);
		std::shared_ptr<LocaObj> p3(p2);
	}


	LOG_TAG("std::unique_ptr 唯一指標");
	{
		{
			std::unique_ptr<LocaObj> p1;
			{
				std::unique_ptr<LocaObj> p2 = std::make_unique<LocaObj>(1);
				LOG_VAR_DESC(p2->ID, " p2->ID");

				LOG("唯一指標轉移控制權使用=運運算元報錯,須使用std::move()");
				// std::unique_ptr<LocaObj> p1 = p2;
				p1 = std::move(p2);
				LOG_VAR_DESC(p1->ID, " p1->ID");

				LOG("測試被std::move的p2是否還有效,可以看到 無效");
				LOG_UNIQUE_PTR_VALID(p2);
				LOG("p2 離開作用域, ID為1的Obj沒有解構");
			}

			LOG("p1 離開作用域, ID為1的Obj解構");
		}
	}


	LOG_TAG("std::unique_ptr 唯一指標的宣告方式");
	{
		std::unique_ptr<LocaObj> p1(new LocaObj(1));
		std::unique_ptr<LocaObj> p2 = std::make_unique<LocaObj>(2);
		std::unique_ptr<LocaObj> p3(std::move(p2));
	}


	LOG_TAG("std::weak_ptr 弱指標");
	{
		std::shared_ptr<LocaObj> p1 = std::make_shared<LocaObj>(1);
		std::weak_ptr<LocaObj> p2 = p1;

		LOG_VAR_DESC(p1->ID, " p1->ID");
		LOG_WEAK_PTR_IF_VALID(p2, LOG_VAR_DESC(tp->ID, " p2->ID"););

		LOG("p1 reset Obj物件被釋放");
		p1.reset();

		LOG("p2 弱指標失效");
		LOG_WEAK_PTR_IF_VALID(p2, LOG_VAR_DESC(tp->ID, " p2->ID"););
	}
}