C++の詳しい解説---スマートポインタ

なぜスマート ポインターがあるのですか?

これまでの知識に基づいて、例外を使用すると、一部のリソースが正常に解放されなくなる可能性があることがわかっています。これは、例外がスローされた後、例外がキャッチされた場所に直接ジャンプするため、次のような非常に重要なコードがスキップされるためです。次の状況:

int div()
{
    
    
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
    
    
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	cout << "delete p1" << endl;
	delete p1;
	cout << "delete p2" << endl;
	delete p2;
}
int main()
{
    
    
	try{
    
    Func();}
	catch (exception& e)
	{
    
    cout << e.what() << endl;}
	return 0;
}

main関数内でfunc関数が呼び出され、func関数内でdiv関数が呼び出されます。func関数では例外はキャッチされませんが、main関数で例外がキャッチされるため、例外が発生した場合、 func 関数内のコードは実行されません。これにより、メモリ リークが発生します。たとえば、次の実行結果が 0 で除算されない場合、実行結果は正しくなります: 0 で除算すると、p1 と p1 が指す
ここに画像の説明を挿入します
空間p2 は正常に解放できません。
ここに画像の説明を挿入します
したがって、この問題を解決するには、この概念を再スローする必要があります。 func 関数で例外をキャッチするコードを追加し、次にキャッチ内のリソースを解放し、最後に例外を再スローします。そして最後に、処理のために main 関数の catch に渡します。たとえば、次のコードです。

int div()
{
    
    
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
    
    
	int* p1 = new int;
	int* p2 = new int;
	try
	{
    
    
		cout << div() << endl;
	}
	catch (exception& e)
	{
    
    
		cout << "delete p1" << endl;
		delete p1;
		cout << "delete p2" << endl;
		delete p2;
		throw invalid_argument("除0错误");
	}
}
int main()
{
    
    
	try{
    
    Func();}
	catch (exception& e)
	{
    
    cout << e.what() << endl;}
	return 0;
}

こうすることで、すべてを0で割ったときにリソースを解放し忘れることがなくなります。 例えば、以下の実行結果です。 0で割っても上記の
ここに画像の説明を挿入します
リソースは正常に解放できることがわかりますが、これは正しいでしょうか。こうやって書くの?new 自体も例外をスローするため、絶対にそうではありません。メモリが不足しているのに new を使用してスペースを申請すると、スペースが開かれて例外がスローされます。では、new をスローした結果はどうなるでしょうか。例外?状況別に見ていきますが、まずp1が例外をスローした場合に何か問題があるのでしょうか?答えは、p1 が例外をスローした場合、キャプチャのために main 関数に直接ジャンプし、p2 はまだ開発されておらず、p1 も正常に開発されていないため、メモリ リークは発生しないということです。開発されるのか?このとき、キャプチャのためmain関数に直接ジャンプしますが、p1は既に空きを確保しているため、p2が空きの確保に失敗すると、p1が申請したリソースが正常に解放されなくなるため、安全のため理由として、p2 も追加します。try が上昇し、try ブロックには次のコードが含まれている必要があります。メモリ アプリケーションが失敗すると、後続の呼び出し関数を実行する必要がないため、ここでのコードは次のようになります。

void Func()
{
    
    
	int* p1 = new int;
	try 
	{
    
    
		int* p2 = new int;
		try{
    
    cout << div() << endl;}
		catch (exception& e)
		{
    
    
			cout << "delete p1" << endl;
			delete p1;
			cout << "delete p2" << endl;
			delete p2;
			throw invalid_argument("除0错误");
		}
	}
	catch (...)
	{
    
    
		//...
	}
}

また、内部の div も例外をスローしますが、この例外は func 関数の一番外側の catch でキャッチされる可能性があります。これは例外処理の際に非常に面倒になりますか? ここでは 2 つの整数変数のみを適用します。では、3 つまたは 4 つ以上ある場合に try catch をネストするにはどうすればよいでしょうか? そのため、例外が連続してスローされると、以前に学習した try catch ステートメントでは対処することが困難になるため、この問題を解決するために、スマート ポインタの概念が提案されました。

スマート ポインタ シミュレーションの実装

まず、スマート ポインターはクラスであり、このクラスはさまざまなデータを処理する必要があるため、このクラスは次のコードのようなテンプレート クラスである必要があります。

template<class T> 
class Smart_Ptr
{
    
    
public:
private:
	T* _ptr;
};

次に、コンストラクターとデストラクターが必要です。コンストラクターにはパラメーターが必要で、このパラメーターを使用して内部 _ptr を初期化します。デストラクターもあります。デストラクターは内部で delete を使用して、ポインターが指すスペースを解放します。以上です。ここでのコードは次のとおりです。

template<class T> 
class Smart_Ptr
{
    
    
public:
	Smart_Ptr(T*ptr)
		:_ptr(ptr)
	{
    
    }
	~Smart_Ptr()
	{
    
    
		delete[] _ptr;
		cout << "~ptr:" <<_ptr <<endl;
	}
private:
	T* _ptr;
};

このスマート ポインター クラスを使用すると、要求されたリソースのアドレスをスマート ポインター オブジェクトに割り当てて、上記の問題を解決できます。たとえば、次のコードです。

void Func()
{
    
    
	int* p1 = new int;
	int* p2 = new int;
	Smart_Ptr<int> sp1 = p1;
	Smart_Ptr<int> sp2 = p2;
	cout << div() << endl;
	cout << "delete p1" << endl;
	delete p1;
	cout << "delete p2" << endl;
	delete p2;
	throw invalid_argument("除0错误");
}

コードの実行結果は以下の通りです:
ここに画像の説明を挿入します
0除算エラーが発生しても、適用された2つのスペースは解放できることがわかります. 原理としては、スマート ポインタ オブジェクトのライフサイクルは Func 関数に属します0除算エラーで例外がスローされると直接main関数にジャンプしますが、このときFunc関数も終了します、クラスオブジェクトの寿命が尽きるとすぐにFunc関数も終了します。終了したらデストラクタを呼び出して領域を解放するので、先ほどの問題は解決しますが、new時にスローされた例外もここで解決できるでしょうか?答えは「はい」で、原理はまったく同じなので、ここでは説明しません。もちろん、コンストラクターとデストラクターだけではニーズを満たすのに十分ではありません。コンストラクターはリソースの保存を担当し、デストラクターはリソースの解放を担当します。さらに、これらのリソースの使用を支援するいくつかの関数も必要なので、3 つ追加できます演算子のオーバーロード関数には、逆参照オーバーロード、-> 演算子のオーバーロード、および角括弧のオーバーロードが含まれます。これら 3 つの関数の実装は次のとおりです。

template<class T> 
class Smart_Ptr
{
    
    
public:
	Smart_Ptr(T*ptr)
		:_ptr(ptr)
	{
    
    }
	T& operator *(){
    
    return *_ptr;}
	T* operator->(){
    
    return _ptr;}
	T& operator[](size_t pos) {
    
     return _ptr[pos]; }
	
	~Smart_Ptr()
	{
    
    
		delete[] _ptr;
		cout << "~ptr:" <<_ptr <<endl;
	}
private:
	T* _ptr;
};

これら 3 つの関数を使用すると、スマート ポインタを使用して、次のコードのように、アドレスが指すコンテンツを変更できます。

int main()
{
    
    
	Smart_Ptr<int> sp1(new int[10]{
    
     1,2,3,4,5 });
	cout << sp1[3] << endl;
	sp1[3]++;
	cout << sp1[3] << endl;
	return 0;
}

コードの実行結果は以下の通りです:
ここに画像の説明を挿入します
ここには内部データ読み込みの機能が実装されていることが分かります. 上記のスマートポインタの形式をRAIIと呼んでいます. RAII(Resource Acquisition Is Initialization)とはオブジェクトの寿命を利用したメソッドですプログラム リソース (メモリ、ファイル ハンドル、ネットワーク接続、ミューテックスなど) を制御するサイクル。オブジェクトの構築時にリソースを取得し、オブジェクトのライフサイクル中有効になるようにリソースへのアクセスを制御し、
オブジェクトの破棄時に最終的にリソースを解放することで、実際にリソースの管理を委託します。オブジェクトを作成します。つまり、リソースとオブジェクトをバインドし、オブジェクトが終了したらリソースを解放します。このアプローチには 2 つの大きな利点があります。 リソースを明示的に解放する必要がありません。このようにして、オブジェクトが必要とするリソースは、その存続期間を通じて有効なままになります。

ライブラリ内のスマート ポインター

ライブラリ内のスマート ポインターを確認する前に、まず次のコードを見てみましょう。

void func2()
{
    
    
	Smart_Ptr<int>sp1(new int(10));
	Smart_Ptr<int>sp2(sp1);
}

このコードを実行すると、実装したスマート ポインター クラスに問題があることがわかります。
ここに画像の説明を挿入します
理由は非常に簡単です。実装したクラスにはコピー コンストラクターがないため、コンパイラーが自動的にコピー コンストラクターを生成し、シャロー コピーを使用して、これにより、2 つのスマート ポインターが同じ空間を指すことになるため、関数呼び出しが終了してオブジェクトのライフサイクルが終了すると、同じ空間を 2 回破棄するために delete が呼び出され、上記のエラーが報告されます。これを解決するには、問題はコピー コンストラクターを自分で実装しなければならないことですが、クラスの目的はポインターのようにすることなので、ここでのコピー コンストラクターは深いコピーにはできません。あるポインターを別のポインターに代入するとき、 two 2 つのポインターがそれぞれ 2 つの空間を指すのではなく、同じ空間を指すため、ここではコピーの構築にディープ コピーを使用できません。では、ライブラリはこの問題をどのように解決するのでしょうか? まず、最も初期の auto_ptr がこの問題をどのように解決できるかを見てみましょう。

auto_ptr

まずは auto_ptr の導入を見てみましょう。
ここに画像の説明を挿入します
ここに画像の説明を挿入します

使用方法は自分で実装したスマートポインタと同じなので、次のコードを書くことができます。

void func2()
{
    
    
	auto_ptr<int>sp1(new int(10));
	auto_ptr<int>sp2(sp1);
}

コードを実行すると、ここでの結果に問題がないことがわかります:
ここに画像の説明を挿入します
しかし、逆参照を使用してポインタが指すコンテンツを表示すると、ここでエラーが報告されることがわかります。たとえば、次のコード:

void func2()
{
    
    
	auto_ptr<int>sp1(new int(10));
	auto_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

コードを実行した結果は次のようになります。

ここに画像の説明を挿入します
このエラーの原因は auto_ptr の実装方法に関連しています。auto_ptr の解決策は、管理権を譲渡し、元のスマート ポインタを空にし、新しいスマート ポインタがこの空間を指すようにすることです。たとえば、次の図:コピー
ここに画像の説明を挿入します
構築 以前はsp1にデータが格納されていましたが、コピー構築後はsp1のデータは以下のようになりました。
ここに画像の説明を挿入します

したがって、上記のエラーの理由は、null ポインターへの逆参照アクセスであるため、上記の形式のコピー構築を実装したい場合は、パラメーター内の const を削除する必要があります (たとえば、次のコード)。

Smart_Ptr(Smart_Ptr<T>& sp)
	:_ptr(sp._ptr)
{
    
    
	sp._ptr = nullptr;
}

これが auto_ptr の実装原理ですが、この形式は誰も使用しないので理解する必要はありません。auto_ptr の使い方があまりにも無理があるため、この形式のスマート ポインタは使用できないと明示的に要求する企業も多くありますが、auto_ptr が使いにくいという問題を解決するために、unique_ptr や share_ptr/weak_ptr が存在します。

unique_ptr

まず、この関数の概要を見てみましょう:
ここに画像の説明を挿入します
このスマート ポインターでサポートされている操作:
ここに画像の説明を挿入します
次に、unique_ptr の使用によって auto_ptr の問題が発生するかどうかを見てみましょう。ここでのコードは次のとおりです。

#include<memory>
void func2()
{
    
    
	unique_ptr<int>sp1(new int(10));
	unique_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

コードを実行すると、次の結果が表示されます:
ここに画像の説明を挿入します
unique_ptr を使用すると問題が発生することがわかりますが、この問題は auto_ptr とは異なります。コピー コンストラクターが削除されているため、問題が発生しています。コピー構築の問題を解決する unique_ptr のアイデアは、コピー構築の使用を直接防ぐことであることがわかります。そのため、ここでの実装ロジックは次のとおりです。

Smart_Ptr(const Smart_Ptr<T>& sp) = delete;

この種の実装ロジックは決して実用的ではないため、あまり深く理解する必要はありませんが、次の形式のスマート ポインターを見てみましょう。

共有_ptr

まず、このスマート ポインターの導入を見てみましょう。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します
上記の使用方法と似ているので、この形式のスマート ポインターが前述の問題を引き起こすかどうかをテストしてみましょう。ここでのコードは次のとおりです。

void func2()
{
    
    
	shared_ptr<int>sp1(new int(10));
	shared_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
	*sp1=20;
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

コードの実行結果は次のとおりです:
ここに画像の説明を挿入します
share_ptr を使用すると、コピーが妨げられたり、コピー後に空白のままになったりすることはなく、このスマート ポインターは通常のポインターと同じ領域を指していることがわかります。私たちのニーズはどのように実現されるのでしょうか? 原理は非常に単純で、参照カウントを使用して実装されます。スマート ポインタを使用してデータを保存する場合、スペースも開き、整数を使用してそれを保存します。このデータはいくつのオブジェクトをポイントしていますか? ポイントされたオブジェクトの数が0、それは破壊されます。このスペースを解放する関数で削除が使用されます。たとえば、下の図では:
ここに画像の説明を挿入します
スマート ポインタは 2 つのスペースを指します。1 つのスペースはデータの保存に使用されます。もう 1 つのスペースは、現在のスペースが次のスペースであることを記録します。いくつかのスマート ポインターによって指されています。現在、オブジェクト sp1 のみがそれを指しています。この空間の現在の数は 1 です。別のオブジェクト sp2 を作成してこの空間を指すと、図は次のようになります:
ここに画像の説明を挿入します
オブジェクトがもう 1 つあるためsp1 オブジェクトのライフサイクルが終了するか、sp1 が他のコンテンツを指すと、count 変数は 2 になります。
ここに画像の説明を挿入します
これが share_ptr の格納原理ですが、ここで問題が発生します。カウント変数のスペースをどのように割り当てるか? 通常の整数変数をオブジェクトに配置できますか? カウント変数の値が変化すると、その空間オブジェクトを指す内部のカウント変数がすべて変化する必要があるため、それは明らかにあり得ません。では、これらのオブジェクトを見つけるにはどうすればよいでしょうか? 敵は隠れており、私も隠れています。明らかにこれです。は難しいのですが、実装するとしたら静的メンバ変数を使って実装できるでしょうか?クラスによってインスタンス化されるオブジェクトの数に関係なく、静的変数は 1 つだけであり、すべてのオブジェクトがこの静的変数を共有するため、可能であるように思えます。その後、1 つのオブジェクトがこの静的変数を変更する限り、他のオブジェクトも追随します。では、これで目的は達成できるでしょうか?実際にはいいえ、静的変数は 1 つのオブジェクトによって変更されたすべてのオブジェクトから表示できますが、このオブジェクトはこのクラスのインスタンス化されたすべてのオブジェクトを参照します。一部のスマート ポインタは整数配列 arr1 を指し、一部のスマート ポインタは整数を指します。配列 arr2、しかし、静的変数が 1 つしかないため、内部のカウント変数はすべて同じ値を持ちます。これは非論理的ですよね? したがって、静的変数を使用してカウントすることは不可能です。そこで、最後に残った方法は、 new in で使用することです。コンストラクターでスペースを開き、スペース内でポイントされるポイントの数を記録し、整数ポインター変数をクラスに追加して、ポインターが開いたスペースを指すようにします。この場合のコンストラクター コードは次のようになります。

template<class T>
class shared_ptr
{
    
    
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{
    
    }
private:
	int* _pcount;
	T* _ptr;
};

コピー構築では、2 つのポインターの値を代入し、整数ポインターが指すコンテンツに 1 つを追加するだけで済みます。たとえば、次のコード:

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

デストラクタがスペースを解放するときは、最初に判断する必要があります。現在のオブジェクトのカウント変数が 1 に等しい場合、両方のスペースを解放します。カウント変数が 1 より大きい場合、カウント値を減算します。1 はここでのコードは次のとおりです。

~shared_ptr()
{
    
    
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
	}
}

残りの 3 つの演算子も同じ理由でオーバーロードされますが、ここでは説明しませんので、コードを参照してください。

T& operator*()
{
    
    
	return *_ptr;
}
T* operator->()
{
    
    
	return  _ptr;
}
T& operator[](size_t pos)
{
    
    
	return _ptr[pos];
}

次に、次のコードを使用してテストできます。

void func2()
{
    
    
	YCF::shared_ptr<int>sp1(new int(10));
	YCF::shared_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
	*sp1=20;
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

コードの実行結果は次のとおりです。
ここに画像の説明を挿入します
結果が正常であることがわかります。デバッグを通じて、sp1 の count 変数が最初は 1 であることがわかります。
ここに画像の説明を挿入します
コピーの構築が完了すると、この 2 つのオブジェクトの count 変数はすべて 2 になります:
ここに画像の説明を挿入します
内部ポインタが指すアドレスも同じなので、実装方法は正しいということになります 次に、代入オーバーロードの実装方法を見てみましょう。オーバーロードでは自分でオーバーロードすることができないので、渡されたオブジェクトとオブジェクト内の _ptr が同じかどうかをまず if 文で判定し、同じであれば何も操作しません。同様に、次の操作を実行します。

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    
    
	if (_ptr != sp._ptr)
	{
    
    

	}
}

ポインタが同じでない場合は、まず現在のクラスの count 変数が 1 であるかどうかを判断します。1 である場合は、オブジェクトの 2 つのスペースを解放し、パラメータ内の 2 つのスペースをポイントし、count 変数を追加します。現在のカウント変数が 1 より大きい場合、独自のカウント変数をデクリメントし、2 つのポインターのポインティングを変更します。このときのコードは次のようになります。

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    
    
	if (_ptr != sp._ptr)
	{
    
    
		if (--(*_pcount) == 0)
		{
    
    
			delete _pcount;
			delete _ptr;
		}	
		_pcount = sp._pcount;
		_ptr = sp._ptr;
		++(*_pcount);
	}
}

次に、次のコードを使用してテストできます。

void func2()
{
    
    
	YCF::shared_ptr<int>sp1(new int(10));
	YCF::shared_ptr<int>sp2(new int(20));
	YCF::shared_ptr<int>sp3(sp1);
	YCF::shared_ptr<int>sp4(sp2);
	cout << "*sp1: " << *sp1 << endl;
	cout << "*sp2: " << *sp2 << endl;
	cout << "*sp3: " << *sp3 << endl;
	cout << "*sp4: " << *sp4 << endl;
	sp1 = sp2;
	cout << "*sp1: " << *sp1 << endl;
	cout << "*sp2: " << *sp2 << endl;
	cout << "*sp3: " << *sp3 << endl;
	cout << "*sp4: " << *sp4 << endl;
}

コードの実行結果は次のとおりです:
ここに画像の説明を挿入します
ここでの実行結果はニーズを満たしていることがわかります。デバッグを通じて、sp1 と sp3 が指すアドレスは割り当て前は同じであり、参照カウントが 2 であることがわかります。 sp2とsp4が指すスペースは同じで、参照カウント値も2です。sp2を
ここに画像の説明を挿入します
sp1に代入すると、sp1、sp2、sp4が指すアドレスが同じで参照カウントが3になることがわかります。 、sp3 の参照カウントは 1 になります: 次に、
ここに画像の説明を挿入します
shared_ptr のシミュレーション実装の完全なコードは次のとおりです。

namespace YCF
{
    
    
	template<class T>
	class shared_ptr
	{
    
    
		public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{
    
    }
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
    
    
			if (_ptr != sp._ptr)
			{
    
    
				if (--(*_pcount) == 0)
				{
    
    
					delete _pcount;
					delete _ptr;
				}	
				_pcount = sp._pcount;
				_ptr = sp._ptr;
				++(*_pcount);
			}
			return *this;
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
    
    
			++(*_pcount);
		}
		~shared_ptr()
		{
    
    
			if (--(*_pcount) == 0)
			{
    
    
				delete _pcount;
				delete _ptr;
			}
		}
		T& operator*(){
    
    return *_ptr;}
		T* operator->(){
    
    return _ptr;}
		T& operator[](size_t pos){
    
    return _ptr[pos];}
	private:
		int* _pcount;
		T* _ptr;
	};
}

スマート ポインターのスレッド セーフティの問題

コードを記述するとき、複数のスレッドが同じリソースを共有する場合がありますが、複数のスレッドに直面した場合、上で実装したスマート ポインターは問題を引き起こすでしょうか? 検証する機能を追加します

int get()
{
    
    
	return *_pcount;
}

この関数は、オブジェクト内の count 変数の値を取得するのに役立ちます。その後、次のコードを使用してテストできます。

void func3()
{
    
    
	int n = 50000;
	YCF::shared_ptr<int> sp1(new int(10));
	thread t1([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<int> sp2(sp1);
			}
		});
	thread t2([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<int> sp3(sp1);
			}
		});
	t1.join();
	t2.join();
	cout << sp1.get() << endl;
}

実行結果が常に 1 である場合は、コードが安全であることを意味します。実行結果が他の値を示している場合は、上記のコードに問題があることを意味します。複数回実行されていることがわかりますが、結果は毎回異なり
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ますでは、なぜそうなるのでしょうか?理由は非常に単純です。複数のスレッドは独立して動作します。処理 1 がカウント変数を ++ するとき、処理 2 もこの変数を使用して ++ しますが、++ は 1 つのステップでは完了できません。また、ある程度の処理が必要です。このステップでは、この変数への 1 の追加が完了していないプロセスが発生します。その後、別のプロセスがこの変数を使用して 1 を追加します。たとえば、カウント変数 x の現在の値は 1 で、プロセス 1 の実行には x かかります。 ++ですが、++は3ステップに分かれています。処理1が最初のステップに到達すると、処理2はxの値を使って++を実行します。実行後の処理1の結果は2、処理2の実行後の結果は2です。実行も2なのでこれは結果的にスマートポインタオブジェクトを2つ作成しましたが、この空間に対応するcount変数は1しか増えませんでしたが、破棄時には問題ないかもしれません通常はcount変数は2減ります, それで、これが問題です。上にあるもの 破壊中に問題があったが、構築中に問題がなかったので、実行結果は 1 より大きくなります。この問題を解決するにはどうすればよいですか?
ここに画像の説明を挿入します
答えは、ミューテックス オブジェクトにロックを追加して、ユーザーがコピーできないようにすることです
ここに画像の説明を挿入します
。また、同じスペースを指すオブジェクトは、スペースの使用に競合がないように同じロックを使用する必要があるため、スマート ポインター オブジェクトを作成するときに、別のスペースを申請する必要があります。ロックを保存するために使用されるため、クラス オブジェクトにロック タイプ ポインタを追加する必要があります。コンストラクタでは、ロック ポインタは新しく作成されたロックを指します。コピー構築中、ロック ポインタは次のようになります。ここでのコードは次のようになります。

template<class T>
class shared_ptr
{
    
    
	public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		,_pmtx(new mutex)
	{
    
    }
private:
	int* _pcount;
	T* _ptr;
	mutex* _pmtx;
};

次に、コピー構築中に、++ の前にロックを追加し、++ の実行が完了したらロックを解除する必要があります。ロックがあると、プロセス 1 がこのコードを実行するとき、プロセス 2 は外で待機することしかできなくなり、その後、プロセス1のロックを解除してから実行し、内部のコードをロックしてコードをロックすると、コピー構築のコードは次のようになります。

shared_ptr(const shared_ptr<T>& sp)
	:_ptr(sp._ptr)
	, _pcount(sp._pcount)
	,_pmtx(sp._pmtx)
{
    
    
	_pmtx->lock();
	++(*_pcount);
	_pmtx->unlock();
}

割り当てのオーバーロードについても同様です。コードは ++ の前にロックされ、++ の後にロックが解除されます。ここでのコードは次のようになります。

void Release()
{
    
    
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
	}
	_pmtx->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    
    
	if (_ptr != sp._ptr)
	{
    
    
		Release();
		_pcount = sp._pcount;
		_ptr = sp._ptr;
		_pmtx = sp._pmtx;
		_pmtx->lock();
		++(*_pcount);
		_pmtx->unlock();
	}
	return *this;
}

ロックを取得した後、上記のコードを実行すると、プログラムを何回実行しても、実行結果は 1 であることがわかります。ロックを追加するとエラーを回避できますが、それでもこれを実行する必要があります
ここに画像の説明を挿入します
。デストラクタでロック. 削除、このように書いてもいいでしょうか?

void Release()
{
    
    
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
		delete _pmtx;
	}
	_pmtx->unlock();
}
~shared_ptr()
{
    
    
	Release();
}

コードを実行すると、次のようなエラーが表示されます:
ここに画像の説明を挿入します
ここに画像の説明を挿入します
理由も非常に単純で、ロックが削除されてもロックはまだ動作状態にあるため、現在ロックを削除できるかどうかを記録する変数フラグを作成します。 if ステートメントでは、フラグの値を変更するだけです。ロックが解除された後、フラグの値を判断します。フラグが true の場合、ロックを削除します。すると、ここでのコードは次のようになります。

void Release()
{
    
    
	bool flag = false;
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
		flag = true;
	}
	_pmtx->unlock();
	if (flag)
	{
    
    
		delete _pmtx;
	}
}
~shared_ptr()
{
    
    
	Release();
}

マルチスレッドのローカル変数はスタックに格納され、各スレッドには独自のスタックがあるため、競合は発生しないため、ここでのフラグにはスレッド セーフティの問題はありません。カウント変数の問題は、この変数がストアドはヒープ上にあります。スレッドがいくつあっても、ヒープは 1 つしかないため、問題が発生する可能性があります。上記の変更により、スマート ポインターの内部カウント変数はスレッドセーフになりますが、スマート ポインターが指すオブジェクトはスマート ポインタはスレッドセーフである必要があります。次のコードを見てみましょう。

struct Date
{
    
    
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
void func3()
{
    
    
	int n = 50000;
	YCF::shared_ptr<Date> sp1(new Date);
	thread t1([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<Date> sp2(sp1);
				sp2->_day++;
				sp2->_month++;
				sp2->_year++;
			}
		});
	thread t2([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<Date> sp3(sp1);
				sp3->_day++;
				sp3->_month++;
				sp3->_year++;
			}
		});
	t1.join();
	t2.join();
	cout << "day: " << sp1->_day << endl;
	cout << "month: " << sp1->_month << endl;
	cout << "year: " << sp1->_year << endl;
}

コードの実行結果は次のとおりです:
ここに画像の説明を挿入します
上記の改善の結果、スマート ポインターのカウント変数は安全になりますが、スマート ポインターが指すオブジェクトは安全にはなりません。原理は次のとおりです。これも非常に単純です。上で行った改善はすべてオブジェクトの内部にあり、スマート ポインターによって指されるオブジェクトはオブジェクトの外側にあるため、この 2 つは互いに何の関係もありません。スレッドセーフなので、変更するときにロックも追加する必要があります。その場合、ここでのコードは次のとおりです。

void func3()
{
    
    
	int n = 50000;
	mutex mtx;//这个也可以被捕捉
	YCF::shared_ptr<Date> sp1(new Date);
	thread t1([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<Date> sp2(sp1);
				mtx.lock();
				sp2->_day++;
				sp2->_month++;
				sp2->_year++;
				mtx.unlock();
			}
		});
	thread t2([&]()
		{
    
    
			for (int i = 0; i < n; i++)
			{
    
    
				YCF::shared_ptr<Date> sp3(sp1);
				mtx.lock();
				sp3->_day++;
				sp3->_month++;
				sp3->_year++;
				mtx.unlock();
			}
		});
	t1.join();
	t2.join();
	cout << "day: " << sp1->_day << endl;
	cout << "month: " << sp1->_month << endl;
	cout << "year: " << sp1->_year << endl;
}

コードの実行結果は次のようになります。
ここに画像の説明を挿入します
この場合、スマート ポインターが指すコンテンツとスマート ポインターの内部カウント変数は両方ともスレッド セーフになります。

ループスマートポインター

まず、listnode という名前のクラスを作成します。このクラスには、2 つの listnode ポインターと、データを保存するための int 変数が含まれています。次に、マーカーとして使用するデストラクターを作成します。ここでのコードは次のとおりです。

struct List_Node
{
    
    
	List_Node* prev;
	List_Node* next;
	int val;
	~List_Node()
	{
    
    
		cout << "~List_Node" << endl;
	}
};

次に、次のコードを使用してテストできます:
ここに画像の説明を挿入します
実行結果が正常であることがわかります。では、shared_ptr を使用してここにポインターを格納しても、正常にコンパイルできますか? 次のコードを見てみましょう。

void func4()
{
    
    
	YCF::shared_ptr<List_Node> n1 = new List_Node;
	YCF::shared_ptr<List_Node> n2 = new List_Node;
	n1->next = n2;
	n2->prev = n1;
}

ただし、クラス内のポインターの型が List_Node* であり、n2 と n1 は両方ともスマート ポインター型であるため、この変更には問題があります。スマート ポインターを通常のポインターに割り当てるにはどうすればよいでしょうか? そうです、エラーが報告されました。問題の解決策も非常に簡単です。List_Node の通常のポインタをスマート ポインタに変更するだけです。コードは次のようになります。

struct List_Node
{
    
    
	YCF::shared_ptr<List_Node> prev;
	YCF::shared_ptr<List_Node> next;
	int val;
	~List_Node()
	{
    
    
		cout << "~List_Node" << endl;
	}
};

コードを実行すると、次の結果が表示されます。
ここに画像の説明を挿入します
上記のコードはデストラクターを呼び出していませんが、これはなぜでしょうか? 分析用の図を描くことができます。まず、各 List_Node は次のように成長します:
ここに画像の説明を挿入します
上記のコードは 2 つの list_node オブジェクトを作成し、それから次のようになります:
ここに画像の説明を挿入します
そして、これら 2 つのオブジェクトをそれぞれ指す 2 つの共有ポインターも作成します。すると、図は次のようになります。次に、
ここに画像の説明を挿入します
別のことを行います。これは、2 つの list_node 内のshared_ptr が相互にポイントするようにすることです。すると、図は次のようになります: すべての
ここに画像の説明を挿入します
内部カウント変数が 2 になります。その後、関数呼び出しが終了します。関数が終了すると、これら 2 つのポインタは破棄されます。共有ポインタがスペースを指すのをやめると、このスペースを指します。スペース内の他の共有ポインタの参照カウントは 1 つ減ります。したがって、上の図は次のようになります。スマート ポインタ内の参照カウントが 0 になる
ここに画像の説明を挿入します
と、ポインタが指すオブジェクトが解放されると言います。つまり、上の図の左側のオブジェクトを解放したい場合は、次のようにする必要があります。まず右側のオブジェクトを放します(オブジェクトが破壊されると、オブジェクト内のスマート ポインタも破壊されるため)、右側のオブジェクトを解放したい場合は、まず左側のオブジェクトを解放する必要があるため、矛盾が生じます。したがって、上記のコードでは最後にデストラクターの呼び出しが示されていないため、shared_ptr の実装ロジックに従って上記の問題を解決することはできません。そのため、C++ では、リソースをポイントできるweak_ptr という新しいスマート ポインターが導入されています。リソースにアクセスします。ただし、リソースを管理することはできません
ここに画像の説明を挿入します
ここに画像の説明を挿入します
。shared_ptr を兄貴分と考えることができ、weak_ptr はその弟分です。weak_ptr は RAII をサポートしていません。つまり、リソースの管理をサポートしていませんが、shared_ptr のコピーはサポートしています。 shared_ptr を使用してweak_ptrを構築することが可能です。その後、次のコードを使用してテストできます。list_nodeの内部部分がshared_ptrによって実装されている場合、次のコードの実行結果はどうなりますか:

void func4()
{
    
    
	YCF::shared_ptr<List_Node> n1 = new List_Node;
	YCF::shared_ptr<List_Node> n2 = new List_Node;
	n1->next = n2;
	n2->prev = n1;
	cout << n1.get() << endl;
	cout << n2.get() << endl;
}

明らかにそれらはすべて 2
ここに画像の説明を挿入します
ですが、 list_node の内部コンテンツがweak_ptrに変更されたらどうなるでしょうか? たとえば、次のコード:

struct List_Node
{
    
    
	std::weak_ptr<List_Node> prev;
	std::weak_ptr<List_Node> next;
	int val;
	~List_Node()
	{
    
    
		cout << "~List_Node" << endl;
	}
};
void func4()
{
    
    
	std::shared_ptr<List_Node> n1 (new List_Node);
	std::shared_ptr<List_Node> n2 (new List_Node);
	n1->next = n2;
	n2->prev = n1;
	cout << n1.use_count() << endl;//得到引用计数的值
	cout << n2.use_count() << endl;
}

コードの実行結果は次のとおりです:
ここに画像の説明を挿入します
ここで参照カウントが 1 になるのは、weak_ptr がポイントするだけで管理を行っていないためです。weak_ptr はスペースを指しており、他の共有の参照カウントは発生しません。空間内のポインタ. value の場合、これがその関数です。シナリオによっては循環参照が発生する可能性があることが判明した場合、shared_ptr の代わりにweak_ptr を使用してポイントする必要があります。weak_ptr はどのように実装されますか? 下を見てみましょう。

弱い_ptr

ここでは、大まかなロジックを実装します。ライブラリ内のスマート ポインタは、実装するものよりも複雑になります。したがって、ここでの実装は、誰もがここでのロジックをよりよく理解できるようにするためのものであり、このコンテナの実装は非常に単純です。内部にはスペースを指すポインター変数が 1 つだけあり、パラメーターなしのコンストラクターはポインターを null に初期化するだけです。この場合のコードは次のようになります。

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

次に、コピー コンストラクターがあります。これは、shared_ptr に渡されたポインターを割り当てるだけです。もちろん、ここに問題がある可能性があります。渡されたshared_ptr 内で指されているスペースが消えている可能性があるため、ここでライブラリが実装されると、次のような問題も発生します。と判断しましたが、ここでは大まかな実装しかないので、あまり考慮しません。

weak_ptr(const shared_ptr<T>& sp)
	:_ptr(sp.get())
	//这里改了一下get是获取内容的地址
{
    
    }

次に、代入のオーバーロードについても同じことが当てはまります。

weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    
    
	_ptr = sp.get();
	return *this;
}

最後に、いくつかの演算子オーバーロード関数を追加します。

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

このようにして、weak_ptr が実装されました。独自のweak_ptr を使用してテストしてみましょう。

struct List_Node
{
    
    
	YCF::weak_ptr<List_Node> prev;
	YCF::weak_ptr<List_Node> next;
	int val;
	~List_Node()
	{
    
    
		cout << "~List_Node" << endl;
	}
};
void func4()
{
    
    
	YCF::shared_ptr<List_Node> n1 (new List_Node);
	YCF::shared_ptr<List_Node> n2 (new List_Node);
	n1->next = n2;
	n2->prev = n1;
	cout << n1.use_count() << endl;//得到引用计数的值
	cout << n2.use_count() << endl;
}

実行結果は次のとおりです。
ここに画像の説明を挿入します
期待どおりであることがわかり、weak_ptr のシミュレーション実装が完了しました。

カスタムデリーター

shared_ptr デストラクターは次のように実装されます。

void Release()
{
    
    
	bool flag = false;
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
    
    
		delete _pcount;
		delete _ptr;
		flag = true;
	}
	_pmtx->unlock();
	if (flag)
	{
    
    
		delete _pmtx;
	}
}
~shared_ptr()
{
    
    
	Release();
}

new を使用して型空間に適用するときは、delete を使用して領域を解放しますが、new を使用して配列空間に適用するときは、delete [ ] を使用して領域を解放します。ユーザーは共有ポインタを指していますか? 単一のデータですか、それとも配列ですか? たとえば、次のコード:

void func5()
{
    
    
	YCF::shared_ptr<string> n1(new string[10]);
}

コードを実行した結果は次のようになります。

ここに画像の説明を挿入します
ここでエラーが直接報告されていることがわかりますが、ここで報告されたエラーは私たちの問題ではありません。次のコードのように、ライブラリで共有ポインターを使用するときにも同じ問題が発生します。

void func5()
{
    
    
	std::shared_ptr<string> n1(new string[10]);
}

コードを実行した結果は次のようになります。
ここに画像の説明を挿入します


ここに画像の説明を挿入します
そこでこの問題を解決するためにカスタム デリーターと呼ばれる概念を提案します. ライブラリ内の文書を観察することでカスタム デリーターの図がわかります. このカスタム デリーターはファンクターに相当し, 削除方法は一部のデータを削除するときに問題が発生する可能性があります。その場合は、この時点で削除方法を提供してください。提供された方法を提供していただければ、提供された方法を使用して削除します。たとえば、上記の配列を削除するときに問題が発生するため、配列のデータを解放するための解放関数を提供することができ、その場合のコードは次のようになります。

template<class T>
struct DeleteArray
{
    
    
	void operator()(const T* ptr)
	{
    
    
		delete[] ptr;
		cout << "delete [] "<<ptr<< endl;
	}
};

次に、このカスタム デリーターを、たとえば次のコードに渡します。

void func5()
{
    
    
	std::shared_ptr<string> n1(new string[10],DeleteArray<string>());
}

もう一度実行すると、何も問題がないことがわかります。
ここに画像の説明を挿入します
また、ファンクターだけでなくラムダ式もここに渡すことができます。たとえば、次のコードです。

void func5()
{
    
    
	std::shared_ptr<string> n1(new string[10], DeleteArray<string>());
	std::shared_ptr<string> n2(new string[10], [](string* ptr) {
    
    delete[] ptr; });
}

また、share_ptr を使用してファイルを開き、カスタム デリーターを渡すときに lambda と fclose を使用してファイルを閉じることもできます。たとえば、次のコードです。

void func5()
{
    
    
	std::shared_ptr<string> n1(new string[10], DeleteArray<string>());
	std::shared_ptr<string> n2(new string[10], [](string* ptr) {
    
    delete[] ptr; });
	std::shared_ptr<FILE> n3(fopen("test.cpp","r"), [](FILE* ptr) {
    
     fclose(ptr); });
}

ここまではライブラリでの使い方ですが、カスタマイズしたデリーターの機能を自分で実装していきます。

カスタムデリーターの実装

ライブラリ内のデリーターはコンストラクターのパラメータリストで渡されますが、ライブラリ内の実装方法が非常に複雑なので、クラステンプレートにデリーターを表すパラメータを追加して、クラス。リリース:

template<class T>
class default_delete
{
    
    
public:
	void operator()(T* ptr)
	{
    
    
		delete ptr;
	}
};
template<class T, class D = default_delete<T>>
class shared_ptr
{
    
    
public:
	// RAII
	// 保存资源
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
	{
    
    }
	//template<class D>//这里就是库实现的方法在构造函数上添加模板
	//shared_ptr(T* ptr = nullptr, D del)
	//	:_ptr(ptr)
	//	, _pcount(new int(1))
	//	, _pmtx(new mutex)
	//{}

	// 释放资源
	~shared_ptr()
	{
    
    
		Release();
	}
	void Release()
	{
    
    
		bool flag = false;
		_pmtx->lock();
		if (--(*_pcount) == 0)
		{
    
    
			//delete _ptr;
			_del(_ptr);

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

		if (flag == true)
		{
    
    
			delete _pmtx;
		}
	}
private:
	T* _ptr;
	int* _pcount;
	mutex* _pmtx;
	D _del;
}

次に、次のコードを使用してテストできます。

void func5()
{
    
    
	YCF::shared_ptr<List_Node, DeleteArray<List_Node>> n2(new List_Node[10]);
}

コードの実行結果は次のとおりです。
ここに画像の説明を挿入します
これは期待される結果を満たしていますが、この実装方法はラムダ式には無効です。ラムダは匿名オブジェクトであり、ここでテンプレートに必要なのは型であるためです。decltype が追加された場合でも、 decltype は実行時に取得されるため機能しません 結果ですが、結果はテンプレートのコンパイル時にのみ取得できます unique_ptr は実装した原則と同じであるため、ラムダ式を unique_ptr に入れることはできません。この記事の全内容を皆さんにご理解いただけると幸いです。

おすすめ

転載: blog.csdn.net/qq_68695298/article/details/131625126