[C++]—C++11 スマート ポインター

序文:

  • 今回は、C++11 の新しい概念である例外ポインターを学習します。

目次

(1) スマートポインタの導入

(2) メモリリーク

1. メモリ リークとメモリ リークの危険性とは何ですか?

2. メモリリークの分類(理解)

3. メモリリークを検出する方法(理解する)

4. メモリリークを回避する方法

 (3) スマートポインタの使い方と原理

1、ライ

2. スマートポインタの原理  

3、std::auto_ptr

4、std::unique_ptr

5、std::shared_ptr

6、weak_ptr

7. デリーター

(4) C++11のスマートポインタとブーストの関係

要約する


(1)スマートポインタの導入

適用されたスペース (つまり、 new によって作成されたスペース) は、 use の終了時に 削除する必要があります 。そうしないと、メモリの断片化が発生します。プログラムの実行中、新しいオブジェクトは デストラクタで 削除されます 、この方法ですべての問題を解決できるわけではありません。 グローバル関数内で新しいオブジェクトが発生する場合があり、プログラマにとってこの方法は精神的な負担になります。 この時点で、スマート ポインターが 役に立ち。 スマート ポインターはクラスであるため、スマート ポインターを使用すると 、この問題を大幅に回避できます。クラスのスコープを超えると、クラスは自動的にデストラクターを呼び出し、デストラクターは自動的にリソースを解放します。したがって、スマート ポインタの動作原理は、関数の終了時にメモリ スペースを自動的に解放し、メモリ スペースの手動解放を回避することです。
まず、次のプログラムに メモリの 問題があるかどうかを分析してみましょう。 注意: MergeSort 関数の問題に注意してください。
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

【説明する】

  1. p1 例外がスローされる前に ポインタp1 には有効なメモリ アドレスがまだ割り当てられていないため、ポインタは null ポインタのままになります。

  2. ポインタには有効なメモリが割り当てられていないため 、p1 後続のコードで削除するのは p1 安全ではありません。

  3. p2 p1 また、例外がスローされた後、プログラム フローは p2 割り当てを続行せずに例外処理ブロックに直接ジャンプするため、ポインタには有効なメモリが割り当てられません 。

  4. div() 関数内の例外は、 main() 関数の例外処理ブロックでスローされ、キャッチされます。

つまり、  new int  メモリ割り当てプロセス中に例外がスローされた場合、ポインタ  p1  と は  p2  初期化されないままになり、割り当てられたメモリは解放されず、メモリ リークが発生します。

(2)メモリリーク

1.メモリ リークとメモリ リークの危険性とは何ですか?

メモリ リークとは : メモリ リークとは、プログラムが過失やエラーにより使用されなくなったメモリを解放できない状況を指します。メモリ リークとは、メモリの物理的な消失を指すのではなく、アプリケーションがメモリの特定のセグメントを割り当てた後の設計エラーにより、メモリの特定のセグメントに対する制御が失われ、メモリが無駄に消費されることを指します。
メモリ リークの害 : オペレーティング システムやバックグラウンド サービスなど、長時間実行されるプログラムでのメモリ リークは大きな影響を及ぼし、メモリ リークにより応答がますます遅くなり、最終的にはフリーズします。
void MemoryLeaks()
{
   // 1.内存申请了忘记释放
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
  
  // 2.异常安全问题
  int* p3 = new int[10];
  
  Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
  
  delete[] p3;
}

【説明する】

上記のコードには、いくつかのメモリ リークと例外の安全性の問題が関係しています。簡単に分析してみましょう。

メモリは割り当てられていますが、解放され忘れられています。

  • 次の行では、 malloc メモリは割り当てられますが、対応する free メモリは解放されません。
int* p1 = (int*)malloc(sizeof(int));
  • 同様に、次の行では、メモリを割り当てるために使用されています new が、 delete メモリを解放するための対応する使用はありません。
int* p2 = new int;
  • これらのメモリ割り当ては解放されず、メモリ リークが発生します。これらの割り当てられたメモリは、プログラムが終了しても解放されません。

例外的なセキュリティ問題:

  • 次の行では、 new 整数の配列を割り当てるために使用しますが、 delete[] メモリを解放するための対応するものはありません。
int* p3 = new int[10];
  • Func() 関数内で例外が発生すると 、その例外delete[] p3 は実行されず、関数が戻るときにメモリ リークが発生します。

例外によるメモリ リーク:

  • Func() 関数内で 例外が発生すると、関数 は実行されず、 delete p1 割り当て  られたメモリが解放されませ ん 。delete p2p1p2

これらの問題を解決するには、次のことを行う必要があります。

  • メモリ リークを避けるために、必要に応じて free または を使用してメモリを解放しますdelete 。
  • try メモリを割り当てた後は、例外が発生したときに割り当てられたメモリを解放できるように、必ずブロックを使用してください 。
  • 例外を処理するときは、すでに解放されたメモリを再度解放しないように注意してください。

要約すると、堅牢なコードを作成するには、メモリ リークやその他の例外の問題を最小限に抑えるために、リソース管理と例外処理に注意を払う必要があります。


2.メモリリークの分類(理解)

C/C++ プログラムでは、通常、メモリ リークの 2 つの側面を考慮します。
ヒープメモリリーク (ヒープリーク)
  • ヒープメモリとは、プログラム実行中に必要に応じてmalloc / calloc / realloc / newなどを通じてヒープから割り当てられるメモリことで、使用後は対応するfreeまたはdeleteを呼び出して削除する必要があります。プログラムの設計ミスによってメモリのこの部分が解放されなかったと仮定すると、この部分の領域は将来使用されなくなり、ヒープ リークが発生します
システムリソースのリーク
  • これは、プログラムがソケット、ファイル記述子、パイプなどのシステムによって割り当てられたリソースを、それらを解放するための対応する関数を使用せずに使用することを意味します。その結果、システム リソースが浪費され、システムのパフォーマンスと重大な低下につながる可能性があります。システムの実行が不安定になる。

3.メモリリークを検出する方法(理解する)

Linux でのメモリ リーク検出: Linux でのメモリ リークを検出 する ツール
Windows でのサードパーティ ツールの使用: VLD ツールの 手順

4.メモリリークを回避する方法

1. プロジェクトの初期段階で適切な設計標準を確立し、適切なコーディング標準を開発し、割り当てられたメモリ領域をそれに見合った方法で解放することを忘れないでください。 ps :これが理想的な状態です。ただし、例外が発生した場合、リリースに注意を払っていても問題が発生する可能性があります。それを確実に管理するには次のスマート ポインターが必要です。
2. RAIIアイデアまたはスマート ポインターを 使用してリソースを管理します。
3. 一部の企業の内部仕様では、内部実装されたプライベート メモリ管理ライブラリを使用します。このライブラリには、メモリ リーク検出オプションが組み込まれています。
4. 何か問題が発生した場合は、メモリ リーク ツールを使用して検出します。 ps : ただし、多くのツールは信頼性が低く、高価です。
【まとめ】
メモリ リークは非常に一般的で、解決策は 2 つあります。
  • 1.予防スマートポインターなど
  • 2.その後、エラーがないか確認します漏れ検出ツールなど

 (3)スマートポインタの使い方と原理

1、ライ

RAII ( Resource Acquisition Is Initialization ) は、オブジェクトのライフサイクルを使用してプログラム リソース (メモリ、ファイル ハンドル、ネットワーク接続、ミューテックスなど) を 制御する単純なテクノロジです。

オブジェクトの構築時にリソースを取得し 、オブジェクトのライフサイクル中有効なままになるようにリソースへのアクセスを制御し、 最後に オブジェクトが破棄されるときにリソースを解放します これにより、リソースを管理する責任を実際にオブジェクトに委任します。このアプローチには、次の 2 つの大きな利点があります。
  • リソースを明示的に解放する必要はありません。
  • このようにして、オブジェクトが必要とするリソースは、その存続期間を通じて有効なままになります。

この時点で、上で示したコードに対して、  RAII の考え方を使用して設計されたSmartPtrクラスのソリューションを提供します。

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			cout << "delete:" << _ptr << endl;
			delete _ptr;
	}

private:
	T* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	cout << div() << endl;
}

int main()
{
	try {
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

出力表示:

【説明する】

このアプローチをアピールするために、この時点で申請されたリソースは自分で管理するのではなく、スマート ポインターに引き渡されます。つまり、スマート ポインターはスマート ポインター オブジェクトを構築するために使用されます。

先ほどの p1、p2、div システム コールの 3 つの状況については、この時点で分析して、そのようなシナリオでどのようになるかを確認します。

  • 出力の表示効果によると、それらはすべて通常のリリースであることがわかります。
  • この時点で、なぜ削除せずに公開したのか不思議に思う人もいます。実際には、このような sp1 と sp2 はローカル オブジェクトであり、ローカル オブジェクトがスコープ外に出ると、そのデストラクターが呼び出されるからです。

sp1 が例外をスローした場合:

  • この new が例外をスローする場合、この new は実際のパラメータであるため、コンストラクタには入りません。実際のパラメータは最初に new を呼び出し、new は演算子 new を呼び出して例外をスローします。直接終了するので、何もありません。リソースを解放する必要がありますか?

sp2 が例外をスローした場合:

  • 2 番目の例外がスローされた場合、ここでスローされた例外について言えば、キャッチ領域に直接ジャンプします。実際には、最初にスタック フレームを終了し、内部のオブジェクトがデストラクターを呼び出します。デストラクターを呼び出すと、SP2 によって管理されているリソースが解放されます。

div() が例外をスローした場合:

  • もう一度見てください。上記のようにdiv が 例外をスローすると、この関数は終了し、これらのローカル オブジェクトが仮想関数を呼び出し、解放が完了します。


2.スマートポインタの原理 

上記の SmartPtr は まだポインタとしての動作を持っていないため、スマート ポインタとは言えません。ポインタは逆参照でき、 ポイントされた空間のコンテンツには -> を介してアクセスすることもできるため、 ポインタのように使用できるように* -> を AutoPtrテンプレート クラスで オーバーロード する必要があります 

T& operator*()
{
	return *_ptr;
}

T* operator->()
{
	return _ptr;
}
  • この時点で、通常はポインタのように使用できます。

スマート ポインターの原理を要約すると、次のようになります。
  • 1. RAII特性
  • 2.ポインターのように動作するように、 operator*operator-> をオーバーロードします

3、std::auto_ptr

auto_ptr ドキュメントの紹介

auto_ptr は 、動的に割り当てられたメモリ リソースを管理するために C++98 標準で導入されたスマート ポインターです。これは、リソースの所有権をある auto_ptr インスタンスから別のauto_ptr インスタンスに転送できるようにする、単純な所有権転送メカニズムを提供します 

auto_ptr について知っておくべき重要な点がいくつかあります 。

  • 所有権の転送:  auto_ptr を 使用すると、割り当て操作を通じてリソースの所有権をあるインスタンスから別のインスタンスに転送できます。これは、リソースが別の auto_ptr に割り当てられると、元の auto_ptr は そのリソースを所有しなくなることを意味します。

int main() 
{
    auto_ptr<int> ptr1(new int(5));
    auto_ptr<int> ptr2;

    ptr2 = ptr1; // 所有权转移

    //cout << *ptr2 << endl; // 输出 5
    cout << *ptr1 << endl; // 错误!ptr1 不再拥有指针,已经转移给了 ptr2

    return 0;
}

出力表示:

【説明する】

  1. 上記のコードでは、ptr1 動的に割り当てられた型ポインターがありint、それをptr2;
  2. auto_ptr の所有権移転機能により、ptr1 この時点ではポインタは所有されなくなりましたが、所有権は に移転されますptr2したがって、ptr1 逆参照を使用しようとすると、未定義の動作が発生します。

  • ダングリング ポインタの問題:  auto_ptr にはダングリング ポインタの問題があります。つまり、リソースの所有権が譲渡された後、元の auto_ptr は null ポインタになりますが、そのリソースは依然として別の auto_ptr によって使用される可能性があり、これにより予期しない動作が発生する可能性があります。

int main() 
{
    auto_ptr<int> ptr(new int(5));

    if (true) 
    {
      auto_ptr<int> otherPtr = ptr;
      //...
    }

   cout << *ptr << endl; // 输出不确定的值,可能导致程序崩溃

    return 0;
}

出力表示:

【説明する】

  1. 上記のコードでは、型ポインターptr が動的に割り当てられていますint 次に、このポインタは に転送されotherPtrifステートメント ブロックが終了した後、otherPtrスコープ外に出てポインタを解放し、次のように設定します。nullptr;
  2. この時点で、ptr それはダングリング ポインタになり、それにアクセスすると未定義の動作が発生します。

  【まとめ】

C++ は、 このプロセスの自動化に役立つスマート ポインターauto_ptr を参照します その後のプログラミング経験 (特にSTL の使用) により、より洗練されたメカニズムが必要であることがわかりました。プログラマーのプログラミング経験とBOOST ライブラリによって提供されるソリューションに基づいて、 C++11では auto_ptr が廃止され、3 つの新しいスマート ポインター ( unique_ptr 、shared_ptr 、およびweak_ptr )が追加されました。すべての新しいスマート ポインターはSTL コンテナーで動作し、セマンティクスを移動します。

4、std::unique_ptr

unique_ptr ドキュメント

auto_ptr に関するこれらの問題のため 、C++11 標準では非推奨になりました。最新の C++ では、 auto_ptr の代わりに unique_ptrを使用することをお勧めします unique_ptr は、移動セマンティクスとカスタム デリーターをサポートしながら、より優れたセマンティクスとセキュリティを提供し、リソース管理をより柔軟で信頼性の高いものにします。

unique_ptrの実現原理 : シンプルかつ失礼なアンチコピー。以下の簡略化されたシミュレーションは、 その原理 UniquePtrを実装します。
	template<class T>
	class unique_ptr
	{
	public:

		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		unique_ptr(unique_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}

		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		//c++11 思路:语法直接支持
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

		//c++98思路:只声明不实现,但是用的人可能会在外面强行定义,所以可以声明为私有
	/*private:
		unique_ptr(const unique_ptr<T>& up);*/

	private:
		T* _ptr;
	};


	void Test_unique()
	{
		unique_ptr<int> up1(new int);
		unique_ptr<int> up2(up1);
	}

出力表示:

組み込みコンパイラーを使用する場合、それがどのように機能するかを確認してください。

【説明する】

  1. std::unique_ptr をコピーして構築しようとすると、コンパイル エラーが発生します。
  2. 問題は、  unique_ptr が 排他的所有権を持つスマート ポインターであることです。これは、  unique_ptr が 所有できるリソースは 1 つだけであり、通常のコピー コンストラクターを介してリソースをコピーすることはできないことを意味します。
  3. moveを使用してリソースの所有権をある unique_ptr から別の unique_ptr に移動することはできますが、直接コピーの構築は許可されません。

コードの変更:

【説明する】

  1. この改訂されたコードでは、コンパイル エラーを回避するために、 move関数 up1 によってリソースの所有権が転送されます up2
  2. 要約すると、エラーは unique_ptr  の排他的所有権の性質によって発生します。リソースの一意の所有者のみが許可されるため、 unique_ptr は 通常のコピー コンストラクターを介してコピーできませんリソースの所有権を譲渡するには、move関数を使用します。

5、std::shared_ptr

共有ptrドキュメント

shared_ptr は、リソース管理における所有権の共有の問題を解決するために導入され ました多くの場合、複数のポインタが同じリソースを共同で所有する必要があり、そのリソースを使用する最後のポインタが解放されるまでそのリソースが破棄されないようにする必要があります。shared_ptr は、リソースの安全な解放を確保しながら、複数のポインターがリソースの所有権を共有できるようにするスマート ポインターの実装を提供します。

shared_ptrの原理は 、参照カウントによって複数の shared_ptrオブジェクト 間でリソースを共有する ことです。例: 教師は夜に退勤する前に通知し、最後に退室する生徒が忘れずにドアをロックできるようにします。
  • 1. Shared_ptr は、リソースが複数のオブジェクトによって共有されている内部的に維持します
  • 2.オブジェクトが破棄される(つまり、デストラクターが呼び出される)と、リソースが使用されなくなったことを意味し、オブジェクトの参照カウントが1 つ減ります。
  • 3.参照カウントが0の場合、あなたがリソースを使用する最後のオブジェクトであり、リソースを解放する必要があることを意味します。
  • 4. 0でない場合、それ自体以外の他のオブジェクトがリソースを使用していることを意味し、リソースを解放できません。そうしないと、他のオブジェクトがワイルド ポインタになります。
template<class T>
	class shared_ptr
	{
	public:

		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))
			,_pmtx(new mutex)
		{}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
			,_pmtx(sp._pmtx)
		{
			AddRef();
		}

		void Release()
		{
			_pmtx->lock();
			bool flag = false;

			if (--(*_pcount) == 0 && _ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;

				flag = true;
			}
			_pmtx->unlock();

			if (flag == true)
			{
				delete _pmtx;
			}
		}

		void AddRef()
		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}

		shared_ptr<T>& operator = (const shared_ptr<T> sp)
		{
			if (_ptr != sp._ptr) //防止自己给自己赋值
			{
				Release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmtx = sp._pmtx;

				AddRef();
			}
			return *this;
		}

		int use_count()
		{
			return *_pcount;
		}


		~shared_ptr()
		{
			Release();
		}

		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;
	};

出力表示:

【説明する】

  1. 上記のコードは、C++11 実装の簡略化されたバージョンを示していますshared_ptr これは、動的に割り当てられたメモリを管理し、共有所有権機能を実装するために使用されるテンプレート クラスです。
  2. 実装方法は、参照カウント技術を使用することです。つまり、同じ動的に割り当てられたメモリを指す共有ポインタの数を記録し、最後のポインタが破棄されたときにメモリを解放します。
  3. スレッドの安全性を確保するために、ミューテックスを使用してカウンターの増分と減分を同期します。
  4. さらに、共有ポインタを通常のポインタと同じように使用できるようにするために、オーバーロードされた逆参照演算子とメンバー アクセス演算子も提供されています。

shared_ptr のスレッド セーフティの問題:
次のプログラムを通じて、 shared_ptrのスレッド セーフティの問題をテストします shared_ptrのスレッド セーフは 2 つの側面に分かれている ことに注意してください
  • 1.スマート ポインタ オブジェクトの参照カウントは、複数のスマート ポインタ オブジェクトによって共有されます。2 つのスレッドのスマート ポインタの参照カウントは同時に ++ または -- です。この操作はアトミックではありません。参照カウント元々1++ を2 回実行しても、まだ2 である可能性があります。このように、参照カウントがめちゃくちゃになります。リソースが解放されなかったり、プログラムがクラッシュしたりする問題が発生します。したがって、スマート ポインターの参照カウント ++および--をロックする必要があります。これは、参照カウント操作がスレッドセーフであることを意味します。
  • 2.スマート ポインターによって管理されるオブジェクトはヒープに保存され、2 つのスレッドによって同時にアクセスされると、スレッド セーフの問題が発生します。
コード表示:
	struct Date
	{
		int _year = 0;
		int _month = 0;
		int _day = 0;

		~Date()
		{}
	};

	void SharePtrFunc(zp::shared_ptr<Date>& sp, size_t n, mutex& mtx)
	{
		cout << sp.get() << endl;
		for (size_t i = 0; i < n; ++i)
		{
			// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
			zp::shared_ptr<Date> copy(sp);
			// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n
			//次,但是最终看到的结果,并一定是加了2n
			{
				unique_lock<mutex> lk(mtx);
				copy->_year++;
				copy->_month++;
				copy->_day++;
			}
		}
	}

	void test_shared_safe()
	{
		zp::shared_ptr<Date> p(new Date);
		
		cout << p.get() << endl;
		
		const size_t n = 50000;
		mutex mtx;
		thread t1(SharePtrFunc, ref(p), n, ref(mtx));
		thread t2(SharePtrFunc, ref(p), n, ref(mtx));

		t1.join();
		t2.join();

		cout << p.use_count() << endl;

		cout << p->_year << endl;
		cout << p->_month << endl;
		cout << p->_day << endl;
	}

出力表示:

  • ロック操作を実行していない場合:

  • ロック操作を実行すると、次のようになります。


shared_ptr の循環参照:

  • 次に、最初に分析と説明用のコードを示します。
	struct ListNode
	{
		int _data;
		zp::shared_ptr<ListNode> _prev;
		zp::shared_ptr<ListNode> _next;

		~ListNode()
		{ 
			cout << "~ListNode()" << endl; 
		}
	};

	void Test_cycle()
	{
		zp::shared_ptr<ListNode> node1(new ListNode);
		zp::shared_ptr<ListNode> node2(new ListNode);

		cout << node1.use_count() << endl;
		cout << node2.use_count() << endl;

		node1->_next = node2;
		node2->_prev = node1;

		cout << node1.use_count() << endl;
		cout << node2.use_count() << endl;
	}

出力表示:

【説明する】

  • 1. 2 つのスマート ポインター オブジェクト、node1およびnode2 は2 つのノードを指し、参照カウントは1になり、手動で削除する必要はありません
  • 2. node1_next はnode2を指しnode2_prev はnode1を指し、参照カウントは2になります
  • 3. Node1Node2は破棄され、参照カウントは1に減りますが、_next は依然として次のノードを指します。ただし、_prev は前のノードも指します。
  • 4.つまり、_nextが破棄され、node2が解放されます。
  • 5.つまり、 _prevが破棄され、node1が解放されます。
  • 6.ただし、_nextは、 nodeのメンバーです。node1が解放されると、_next は破棄されます。Node1は、 _prevによって管理され、 _prev は、 node2のメンバーであるため、これは循環参照と呼ばれ、誰も解放しません。

循環参照の問題により、プログラムが Test_cycle() を終了しても、2 つのノードの参照カウントは 0 にならないため、それらのデストラクターは呼び出されません。これは、デストラクター内の出力ステートメントが実行されず、メモリ リークのリスクが生じる可能性があることを意味します。

循環参照の問題を回避するために、_prev および _next メンバー変数をweak_ptrにすることができます。これはshared_ptrの弱参照であり、参照カウントは増加しません。このように、循環リンクリストでは、weak_ptr を利用して強参照関係を解消し、循環参照によるメモリリークを防ぎます。

  • 循環参照の問題を解決するサンプルコードは次のとおりです。

まず、weak_ptr を手動で実装する か、ライブラリで提供されているweak_ptrを使用します。ここでは、手動で実装しました。

    template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		T* get()
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};

修正されたコードは次のとおりです。

	struct ListNode
	{
		int _data;
		zp::weak_ptr<ListNode> _prev;
		zp::weak_ptr<ListNode> _next;


		~ListNode()
		{ 
			cout << "~ListNode()" << endl; 
		}
	};

	void Test_cycle()
	{
		zp::shared_ptr<ListNode> node1(new ListNode);
		zp::shared_ptr<ListNode> node2(new ListNode);

		cout << node1.use_count() << endl;
		cout << node2.use_count() << endl;

		node1->_next = node2;
		node2->_prev = node1;

		cout << node1.use_count() << endl;
		cout << node2.use_count() << endl;
	}

出力表示:

【説明する】

  1. 修復されたコードでは、ListNode構造体のメンバー変数 _prev と _next をzp::weak_ptr型に変更します。これにより、次のノードへの強い参照が追加されなくなり、循環参照の問題が回避されます。
  2. プログラムがTest_cycle()関数を終了すると、node1node2 の参照カウントが0 に下がり、デストラクターが ~ListNode() 呼び出され、関連するメモリ リソースが正しく解放されるため、メモリ リークの問題が回避されます。

6、weak_ptr

上記のshared_ptrの説明では、  weak_ptrを使用しました。次回からは正式に導入していきます。

基本的な紹介:

  • weak_ptr は、オブジェクトのライフサイクルを制御しないスマート ポインタでありshared_ptrによって管理されるオブジェクトを指します。
  • オブジェクトのメモリ管理は、強参照されたshared_ptrによって実行されますweak_ptr は、管理対象オブジェクトへのアクセス手段のみを提供します。
  • weak_ptr設計の目的は、 shared_ptr の作業を支援するために、 shared_ptrと連携するスマート ポインタを導入することです。これは、 shared_ptrまたは別のweak_ptrオブジェクトからのみ構築できますその構築と破棄によって、参照カウントが増減することはありません
  • weak_ptr は、shared_ptr が相互に参照するときのデッドロックの問題を解決するために使用されます。2つのshared_ptr が相互に参照する場合、これら 2 つのポインターの参照カウントが0 になることはなく、リソースが解放されることはありません。
  • これはオブジェクトへの弱参照であり、オブジェクトの参照カウントは増加せず、shared_ptr に変換でき、shared_ptr を直接割り当てることができ、 lock関数を呼び出すことでshared_ptrを取得できます。

次に、コードを簡単に見てみましょう。

class B;
class A
{
public:
	shared_ptr<B> pb_;
	~A()
	{
		cout << "A delete\n";
	}
};

class B
{
public:
	shared_ptr<A> pa_;
	~B()
	{
		cout << "B delete\n";
	}
};

void fun()
{
	shared_ptr<B> pb(new B());
	shared_ptr<A> pa(new A());

	pb->pa_ = pa;
	pa->pb_ = pb;

	cout << pb.use_count() << endl;
	cout << pa.use_count() << endl;
}
int main()
{
	fun();
	return 0;
}

出力表示:

【説明する】

  1. fun関数内のpaと pb は相互に参照しており、2 つのリソースの参照数は2であることがわかります。関数を飛び出す場合、次の時点で 2 つのリソースの参照数は 1 減りますスマート ポインタpapbは破壊されます。
  2. ただし、2 つの参照カウントはまだ1であるため、関数が飛び出すときにリソースが解放されません ( AB のデストラクターが呼び出されません)。どちらか 1 つがweak_ptr に変更された場合は、 shared_ptr pb_ をクラスに置きます。あ;
weak_ptr pb に変更すると、実行結果は次のようになります。

  • この場合、リソース B の参照は最初は1だけですが、pbが破壊されるとB のカウントは0になりBが解放されます。B が解放されると、 Aのカウントも1 減ります同時に、paが破壊されるAのカウントが減り、カウントが 1 減算され、A のカウントが0になりAが解放されます。

: weak_ptr を介してオブジェクトのメソッドに直接アクセスすること はできません。最初にそれをshared_ptr に変換する必要があります。


7. デリーター

オブジェクトが 新しいものではない場合、 スマート ポインターを介してどのように管理できるでしょうか? 実際、 shared_ptr は この 問題を解決するためにデリーターを設計しました。

1.定義

  1. スマート ポインター デリーター (デリーター) は、スマート ポインターが破棄されたときに実行されるカスタム操作を指します。
  2. デリーターは、スマート ポインターによって管理されているリソースを解放するときに、追加のクリーンアップ作業やカスタム ロジックを実行できます。

  • Lambda 式をデリーターとして使用する例を次に示します。
int main()
{
    int* p = new int(10);
    std::shared_ptr<int> sp(
        p, 
        [](int* ptr) 
        { 
            std::cout << "deleting pointer " << ptr << std::endl; delete ptr; 
        });

    // 输出共享指针的引用计数
    std::cout << "sp use_count: " << sp.use_count() << std::endl;

    // 手动将共享指针的引用计数减 1
    sp.reset();

    return 0;
}

出力表示:

【説明する】

  1. 上記のコードは、p整数変数を指す共有ポインターを作成し、デリーター ラムダ式を使用して、削除されたオブジェクトのアドレスを出力し、ヒープに割り当てられたメモリを解放します。
  2. 呼び出し後sp.reset()、参照カウントはゼロになり、メモリを解放するためにデリーターが呼び出されます。

(4) C++11のスマートポインタブーストの関係

std::shared_ptrまず、C++11 では、や などのスマート ポインターが標準ライブラリに導入されましたstd::unique_ptrこれらのスマート ポインターは、リソースの所有権を管理するためのメカニズムを提供します。これにより、メモリ管理が自動的に実行され、リソースを手動で解放する手間が回避されます。C++11 のスマート ポインターは、新しい言語機能とライブラリ サポートを導入することによって実装されます。

Boost は、広くテストされ使用されている高品質の C++ コードの大規模なコレクションを提供する人気の C++ 拡張ライブラリです。C++11 標準でスマート ポインターが導入される前に、Boost はすでに、boost::shared_ptrboost::scoped_ptrなどを含む独自のスマート ポインター ライブラリを提供していました。これらのスマート ポインターは広く使用されており、C++ コミュニティで高く評価されています。

実際、C++11 標準ライブラリのスマート ポインターは、Boost スマート ポインターの影響を受け、インスピレーションを受けています。C++11 標準のstd::shared_ptrsumの設計と機能は、基本的にstd::unique_ptrBoost のboost::shared_ptrsumと非常によく似ていますboost::scoped_ptrC++11 スマート ポインターには、移動セマンティクスやカスタム デリーターなどのいくつかの新機能と改善点も導入されており、パフォーマンスと柔軟性が向上します。


要約する

以上でスマートポインタの説明は終わりです。次に、この記事を簡単に振り返ってまとめてみましょう。

スマート ポインタは、動的に割り当てられたリソースを自動的に管理するために使用されるポインタです。自動メモリ管理を提供することで、メモリ リークやダングリング ポインタなどの一般的なリソース管理の問題を軽減できます。

一般的なスマート ポインターの種類:

  • std::shared_ptr: 複数のポインタが同じメモリ リソースを共有できるようにし、メモリ管理に参照カウントを使用します。
  • std::unique_ptr: 排他的ポインタ。1 つのポインタだけがリソースにアクセスできることを保証します。移動セマンティクスがあり、所有権の転送に使用できます。
  • std::weak_ptr: 弱い参照ポインター。std::shared_ptr循環参照によって引き起こされるリソース リークの問題を解決するために使用されます。

スマート ポインターの利点:

  • リソースを自動的に解放する: スマート ポインターは、デストラクターを通じて管理対象リソースを自動的に解放し、リソースを手動で解放する面倒なプロセスを回避します。
  • メモリ リークを回避する: スマート ポインタは参照カウントまたは排他的所有権を使用して、リソースが使用されなくなったときにリソースが正しく解放されるようにし、メモリ リークを回避します。
  • セキュリティの向上: スマート ポインターを使用すると、ダングリング ポインターやワイルド ポインターの問題が軽減され、プログラムのセキュリティと安定性が向上します。

以上がこの記事の全内容です、ご視聴、応援してくださった皆様、誠にありがとうございました!

 

おすすめ

転載: blog.csdn.net/m0_56069910/article/details/132595213