智慧指標

酱油黑龙發表於2024-08-29

智慧指標

在C++中管理動態記憶體可以使用new和delete,但透過這種方式得到的指標(裸指標)是容易忘記釋放的進而導致記憶體洩漏。因此C++標準中提供了智慧指標shared_ptrweak_ptrunique_ptr來進行動態記憶體的管理。智慧指標的設計滿足了RAII(Resource Acquisition Is Initialization)的特性,即資源獲取即初始化,這使得程式猿能夠節省更多找記憶體洩漏的時間去實現需要的功能。

在這些智慧指標中shared_ptr和weak_ptr是一起用於管理shared_ptr智慧指標物件的,而unique_ptr智慧指標是單獨管理動態記憶體的。

智慧指標標頭檔案

這幾個智慧指標模板類的標頭檔案都是memory

shared_ptr

shared_ptr是共享型智慧指標,其內部有引用計數器,這使得多個shared_ptr物件能指向同一個物件並且僅當引用計數為0時釋放物件的記憶體。

shared_ptr初始化

  1. 使用new初始化
shared_ptr<int> test1(new int(100))
  1. 使用更安全的函式模板make_shared(params)來初始化
shared_ptr<int> test2 = make_shared<int>(100);

shared_ptr常用操作

						成員函式
[shared_ptr<T> object]------------[use_count()]檢視引用計數
          |                  |-----[unique()]檢視智慧指標是否獨佔某個指向的物件
          |                  |-----[reset()/reset(param)]置空或指向其他物件
          |                  |-----[*]*運算子過載,用於模擬指標相關的操作
          |                   -----[get()]獲取裸指標
          |  刪除器
           -----------shared<T>(new T, deleter)初始化shared_ptr指定刪除器,當引用計數為0後呼叫刪除器函式
use_count()成員函式

檢視引用計數,使用use_count()成員函式,這個函式主要用於除錯目的,效率可能不高。當使用shared_ptr進行複製構造,複製賦值運算子處理後,引用計數都會+1。

舉例:

#include <iostream>
#include <memory>
int main()
{
	using namespace std;
	cout << "-------------------------" << endl;
	shared_ptr<int> test1 = make_shared<int>(100);
	cout << "初始化shared_ptr,引用計數為:" << test1.use_count() << endl;
	cout << "-------------------------" << endl;
	shared_ptr<int> test2 = test1;
	cout << "使用複製構造,引用計數為:" << test1.use_count() << endl;
	cout << "-------------------------" << endl;
	shared_ptr<int> test3;
	test3 = test1;
	cout << "使用複製賦值運算子,引用計數為:" << test1.use_count() << endl;
	cout << "-------------------------" << endl;
	return 0;
}

結果:

-------------------------
初始化shared_ptr,引用計數為:1
-------------------------
使用複製構造,引用計數為:2
-------------------------
使用複製賦值運算子,引用計數為:3
-------------------------
unique()成員函式

這個函式是檢視shared_ptr物件是否獨佔某個物件,如果是則返回true,否則返回false。

舉例

#include <iostream>
#include <memory>
int main()
{
	using namespace std;
	cout << "-------------------------" << endl;
	shared_ptr<int> test1 = make_shared<int>(100);
	if (test1.unique() == true)
	{
		cout << "test1 is unique" << endl;
	}
	cout << "-------------------------" << endl;
	return 0;
}

結果:

-------------------------
test1 is unique
-------------------------
reset()與reset(param)成員函式

​ reset成員函式有兩個過載,分別是帶參過載和不帶引數過載。帶參過載的reset使智慧指標指向新的物件,並使得原先物件的引用計數-1;不帶引數的reset使原先指向物件的引用計數-1,並將智慧指標物件置空。

舉例

#include <iostream>
#include <memory>
int main()
{
	using namespace std;
	cout << "-------------------------" << endl;
	shared_ptr<int> test1 = make_shared<int>(100);
	test1.reset();
	if (test1 == nullptr)
	{
		cout << "智慧指標test1置空" << endl;
	}
	cout << "-------------------------" << endl;
	test1.reset(new int(200));
	if (test1 != nullptr)
	{
		cout << "智慧指標test1透過reset(param)成員函式指向新的物件" << endl;
	}
	return 0;
}

結果

-------------------------
智慧指標test1置空
-------------------------
智慧指標test1透過reset(param)成員函式指向新的物件
*解引用

*是被過載的運算子,透過使用它可以對指標解引用。

舉例

#include <iostream>
#include <memory>
int main()
{
	using namespace std;
	cout << "-------------------------" << endl;
	shared_ptr<int> test1 = make_shared<int>(100);
	cout << "*解引用: " << *test1 << endl;
	cout << "-------------------------" << endl;
	return 0;
}

結果

-------------------------
*解引用: 100
-------------------------
get()成員函式

get()成員函式返回保護的裸指標,在獲取到這個裸指標後不要用它去初始化新的shared_ptr物件,因為初始化新的智慧指標後,引用計數器變為多個,當多個引用計數器=0時會釋放多次記憶體導致程式出錯。

舉例

#include <iostream>
#include <memory>
#include <typeinfo>
int main()
{
	using namespace std;
	cout << "-------------------------" << endl;
	shared_ptr<int> test1 = make_shared<int>(100);
	cout << "test1的型別: " << typeid(test1).name() << endl;
	cout << "-------------------------" << endl;
	auto ptr = test1.get();
	cout << "ptr1的型別: " << typeid(ptr).name() << endl;
	return 0;
}

結果

-------------------------
test1的型別: class std::shared_ptr<int>
-------------------------
ptr1的型別: int *
swap()成員函式

swap()用於交換兩個智慧指標指向的物件。

舉例

#include <iostream>
#include <memory>
#include <string>
int main()
{
	using namespace std;
	cout << "-------------------------" << endl;
	shared_ptr<string> ptr1 = make_shared<string>("shared_ptr1");
	shared_ptr<string> ptr2 = make_shared<string>("shared_ptr2");
	ptr1.swap(ptr2);
	cout << "*ptr1: " << *ptr1 << endl;
	cout << "*ptr2: " << *ptr2 << endl;
	cout << "-------------------------" << endl;
	return 0;
}

結果

-------------------------
*ptr1: shared_ptr2
*ptr2: shared_ptr1
-------------------------
刪除器

shared_ptr刪除器就是使用程式猿自己寫的刪除器函式或者lambda表示式在智慧指標物件引用計數為0時呼叫。

舉例

#include <iostream>
#include <memory>
using namespace std;

/*
* @brief 刪除器函式
*/
void deleter(int* p)
{
	cout << "呼叫刪除器函式" << endl;
	delete p;
}

int main()
{
	cout << "-------------------------" << endl;
	shared_ptr<int> ptr(new int(100), deleter);
	//引用計數=0,呼叫刪除器
	ptr.reset();
	cout << "-------------------------" << endl;
	ptr = shared_ptr<int>(new int(200), [](int* p) 
		{
			cout << "lambda表示式刪除器" << endl;
			delete p; 
		});
	//引用計數=0,呼叫刪除器
	ptr.reset();
	cout << "-------------------------" << endl;
	return 0;
}

結果

-------------------------
呼叫刪除器函式
-------------------------
lambda表示式刪除器
-------------------------
注意事項

當使用shared_ptr來管理陣列物件時要寫刪除器,因為shared_ptr預設的刪除器不會釋放陣列物件。當遇到這種情況時有三種方式能正常釋放記憶體

舉例

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	cout << "-------------------------" << endl;
	//方法1.用lambda表示式或者刪除器函式來處理
	shared_ptr<int> ptr1(new int[10], [](int* p)
		{
			cout << "釋放int陣列的記憶體" << endl;
			delete[]p;
		});
	//引用計數=0
	ptr1.reset();
	cout << "-------------------------" << endl;
	//方法2.呼叫標準庫的default_delete
	shared_ptr<int> ptr2(new int[10], default_delete<int[]>());
	cout << "-------------------------" << endl;
	//方法3.在尖括號中也加上[],這樣使用預設刪除器也能正常釋放記憶體
	shared_ptr<int[]> ptr3(new int[10]);

	return 0;
}

結果

-------------------------
釋放int陣列的記憶體
-------------------------
-------------------------

weak_ptr

weak_ptr是用來輔助shared_ptr工作的,weak_ptr通常用於繫結shared_ptr指向的物件並獲取強引用計數及初始化新的shared_ptr物件。

weak_ptr初始化

初始化weak_ptr物件有兩種方式,一種是用shared_ptr物件初始化,另一種是用另一個weak_ptr物件初始化 。

舉例

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	shared_ptr<int> s_ptr = make_shared<int>(100);
	cout << "-------------------------" << endl;
	weak_ptr<int> w_ptr1(s_ptr);
	cout << "使用shared_ptr物件初始化的weak_ptr: " << *(w_ptr1.lock()) << endl;
	cout << "-------------------------" << endl;
	weak_ptr<int> w_ptr2(w_ptr1);
	cout << "使用weak_ptr初始化的weak_ptr: " << *(w_ptr2.lock()) << endl;

	return 0;
}

結果

-------------------------
使用shared_ptr物件初始化的weak_ptr: 100
-------------------------
使用weak_ptr初始化的weak_ptr: 100

weak_ptr常用操作

				常用函式
[weak_ptr]------------------[use_count()] 強引用計數
                    |-------[expired()] 判斷強引用計數是否為0
                    |-------[reset()] 重置weak_ptr物件,原物件弱引用計數-1
                     -------[lock()] 獲取監視的shared_ptr,用於初始化shared_ptr物件

use_count()成員函式

weak_ptr中的use_count()成員函式的功能和shared_ptr中的同名函式一樣,都是用於獲取shared_ptr的引用計數。

舉例

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	shared_ptr<int> s_ptr = make_shared<int>(100);
	shared_ptr<int> s_ptr2 = s_ptr;
	cout << "-------------------------" << endl;
	weak_ptr<int> w_ptr(s_ptr);
	cout << "當前shared_ptr中的引用計數為: " << s_ptr.use_count() << endl;
	cout << "當前weak_ptr中的引用計數為: " << w_ptr.use_count() << endl;
	cout << "-------------------------" << endl;

	return 0;
}

結果

-------------------------
當前shared_ptr中的引用計數為: 2
當前weak_ptr中的引用計數為: 2
-------------------------

expired()成員函式

weak_ptr物件是用於監視shared_ptr物件的,weak_ptr中的expired用於判斷監視的shared_ptr物件中的引用計數是否為0,如果計數=0則返回true,反之返回false。

舉例

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	shared_ptr<int> s_ptr = make_shared<int>(100);
	cout << "-------------------------" << endl;
	weak_ptr<int> w_ptr(s_ptr);
	if (!w_ptr.expired())
	{
		cout << "此時shared_ptr引用計數不等於0" << endl;
	}
	cout << "-------------------------" << endl;
	s_ptr.reset();
	if (w_ptr.expired())
	{
		cout << "此時shared_ptr引用計數為0" << endl;
	}
	return 0;
}

結果

-------------------------
此時shared_ptr引用計數不等於0
-------------------------
此時shared_ptr引用計數為0

reset()成員函式

reset()成員函式用於將weak_ptr物件置空,不影響指向之前物件的強引用數量,但弱引用數量會減1。

lock()成員函式

lock()成員函式用於獲取監視的shared_ptr智慧指標,可以用lock()函式來初始化新的shared_ptr物件。

舉例

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	shared_ptr<int> s_ptr = make_shared<int>(100);
	cout << "-------------------------" << endl;
	weak_ptr<int> w_ptr(s_ptr);
	shared_ptr<int> s_ptr2 = w_ptr.lock();
	cout << "透過weak_ptr獲取的shared_ptr物件解引用的結果為: " << *s_ptr << endl;
	cout << "-------------------------" << endl;
	return 0;
}

結果

-------------------------
透過weak_ptr獲取的shared_ptr物件解引用的結果為: 100
-------------------------

shared_ptr和weak_ptr的內部細節

當使用建立了shared_ptr物件並讓其監視了一個T型別的物件後,控制塊就被shared_ptr物件建立了出來,而shared_ptr物件內部的兩個指標就分別指向T物件和對應的控制塊物件,當用shared_ptr物件初始化weak_ptr物件後,weak_ptr物件也用兩個指標分別指向T和控制塊物件。其中強引用計數是指向T物件的shared_ptr物件總數,而弱引用計數是指向T物件的weak_ptr物件總數。

使用shared_ptr和weak_ptr的注意事項

在成員函式中要用shared_from_this()函式來返回this指標

這一部分我建議大家先看錯誤案例分析及如何解決後再去記使用方法。

enable_shared_from_this及shared_from_this()用法

//標頭檔案memory
#include <memory>
class object: public std::enable_shared_from_this<object>
{
  shared_ptr<object> getself()
  {
      return shared_from_this();
  }
};

錯誤案例及分析

//成員函式返回this的錯誤做法
#include <iostream>
#include <memory>
using namespace std;
class test
{
public:
	shared_ptr<test> getThis()
	{
		return shared_ptr<test>(this);
	}
};
int main()
{

	cout << "-------------------------" << endl;
	shared_ptr<test> s_ptr = make_shared<test>();
	shared_ptr<test> s_ptr2 = s_ptr->getThis();
	cout << "s_ptr的引用計數為:" << s_ptr.use_count() << endl;
	cout << "s_ptr2的引用計數為:" << s_ptr2.use_count() << endl;
	cout << "-------------------------" << endl;
	return 0;
}

結果

-------------------------
s_ptr的引用計數為:1
s_ptr2的引用計數為:1
-------------------------

17行建立了智慧指標物件s_ptr,而在18行建立s_ptr2時問題出現了。this指標是裸指標,這就導致在使用getThis()時建立了另一個新的引用計數的shared_ptr物件,這就使s_ptr和s_ptr2物件的引用計數不是同一個,最終導致在銷燬s_ptr和s_ptr2時會導致釋放兩次記憶體報錯。

如何解決這個錯誤?

在C++標準庫中提供了一種類别範本enable_shared_from_this

相關文章