C++の記事でスマートポインタの魅力がわかる

要約

静的メモリとスタック メモリに加えて、各プログラムには空き領域またはヒープと呼ばれるメモリ プールもあることがわかっています。Programs use the heap to store dynamic allocated objects, that is a objects that are allocated when the program is running. 動的オブジェクトが使用されなくなったら、コードで明示的にそれらを破棄する必要があります。

C++ では、動的メモリ管理は、new と delete の 1 組の演算子で実現されます。new: 動的メモリ内のオブジェクトにスペースを割り当て、オブジェクトへのポインタを返します。動的排他ポインタへのポイントを削除し、オブジェクトを破棄します。それに関連付けられているメモリを解放します。

動的メモリ管理には、多くの場合、メモリの解放を忘れてメモリ リークが発生するという 2 つの問題があります。もう 1 つは、メモリを参照するポインタがまだあるときにメモリを解放し、不正なメモリを参照するポインタが生成されます。

動的メモリをより簡単に (より安全に) 使用するために、スマート ポインターの概念が導入されています。スマート ポインターは通常のポインターのように動作しますが、ポイント先のオブジェクトを自動的に解放するという重要な違いがあります。

スマートポインタの原理

RAII : オブジェクトのライフサイクルを使用して、プログラム リソースを制御します。主に、オブジェクトのコンストラクターを介してリソース管理権限を取得し、次にデストラクタを介して管理対象リソースを解放します。原則は、リソースの管理責任をオブジェクトに委ねることです

//RAII
template<class T>
class SmartPtr
{
    
    
public:
	//构造函数获取资源管理权
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{
    
    }
	//通过析构函数释放资源
	~SmartPtr()
	{
    
    
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};
class A
{
    
    
private:
	int _a = 10;
};

void test()
{
    
    
	//错误写法
	int* ptr = new int[10];
	SmartPtr<int> sp(ptr);
	//立即初始化--申请资源时就立即绑定资源
	SmartPtr<int> sp2(new int);
	SmartPtr<A> sp3(new A);
}

これはスマート ポインター オブジェクトではありません。スマート ポインターは次の条件を満たす必要があります。

  1. RAIIのアイデアを実現
  2. ポインターと同じように使用されます。たとえば、* デリファレンスと -> 操作をサポートする必要があります。

次の操作のオーバーロードをクラスに追加する必要があります


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

スマート ポインターと通常のポインターの違いの 1 つは、スマート ポインターは手動で領域を解放する必要がないことです。

void test()
{
    
    
	//智能指针--编译器调用析构自动释放资源--不存在内存泄漏
	SmartPtr<A> sp(new A);
	(*sp)._a = 10;
	sp->_a = 100;

	//普通指针--手动释放内存
	int* p = new int;
	A* pa = new A;
	*p = 1;
	pa->_a = 10;
	//return  //提前结束普通指针就会导致内存泄漏
	delete p;
	delete pa;
}

C++ 標準ライブラリでのスマート ポインターの使用

ライブラリ内のスマート ポインターは、auto_ptrunique_ptrshare_ptr
に分割されます。これらはすべて、#include <memory>使用するヘッダー ファイルを導入する必要があります。

auto_ptr

auto_ptr は欠陥のあるスマート ポインターです (無効)

#include <memory>
using namespace std;
void test()
{
    
    
	auto_ptr<int> ap(new int);
	auto_ptr<int> ap2(new int(2));
	*ap = 10;
	*ap2 = 20;
}

auto_ptr ポインターが割り当てられると、リソースが転送されます。その目的は、複数のスマート ポインターが同じメモリ リソースを指さないようにすることです。しかし、この設計は明らかに私たちのニーズを満たしていません.
ここに画像の説明を挿入
単純にauto_ptrの実装をシミュレートして、最下層がリソースの権利をどのように転送するかを見てみましょう.

//实现auto_ptr
template<class T>
class Auto_ptr
{
    
    
public:
	Auto_ptr(T* ptr)
		:_ptr(ptr)
	{
    
    }
	~Auto_ptr()
	{
    
    
		if (_ptr)
			delete _ptr;
	}

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

	Auto_ptr(Auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
    
    
		//资源管理权转移 
		ap._ptr = nullptr;
	}

	Auto_ptr<T>& operator=(Auto_ptr<T>& ap)
	{
    
    
		if (this != &ap)
		{
    
    
			if (_ptr)
				delete _ptr;
			//资源管理权转移 
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		return *this;
	}
private:
	T* _ptr;
};

unique_ptr

unique_ptr スマート ポインターは、アンチ コピーによるリソース管理権限の譲渡の問題を解決します ---- 削除関数として unique_ptr 代入演算子関数とコピー コンストラクターを設定します

void test()
{
    
    
	unique_ptr<int> up(new int(10));
	unique_ptr<int> up2(up);//error
	unique_ptr<int> up3(new int(20));
	up = up3; //error
}

エラーの理由: コピーの作成と代入のオーバーロード関数の両方が削除された関数です。
ここに画像の説明を挿入
基になる実装:

template<class T>
class Unique_ptr
{
    
    
public:
	Unique_ptr(T* ptr)
		:_ptr(ptr)
	{
    
    }

	Unique_ptr(const Unique_ptr<T>& up) = delete;
	Unique_ptr<T>& operator=(const Unique_ptr<T>& up) = delete;

	~Unique_ptr()
	{
    
    
		if (_ptr)
		{
    
    
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};

shared_ptr

shared_ptr は C++11 で提供される新しいスマート ポインターであり、リソース管理のパーミッション転送の問題を解決するだけでなく、信頼性の高いコピー機能も提供します。

class A
{
    
    
public:
	int _a = 10;
	~A()
	{
    
    
		cout << "~A()" << endl;
	}
};

void test()
{
    
    
	shared_ptr<A> sp(new A);
	shared_ptr<A> sp2(new A);
	shared_ptr<A> sp3(sp2);//ok
	sp3 = sp;//ok
	sp->_a = 100;
	sp2->_a = 1000;
	sp3->_a = 10000;
	cout << sp->_a << endl;
	cout << sp2->_a << endl;
	cout << sp3->_a << endl;
}

実行結果:
ここに画像の説明を挿入
リソースが適用されるほど、リソースが解放されることがわかりました.このとき、sp と sp3 はリソースを共有しており、sp3 を変更することは sp を変更することと同じです. したがって、どちらも最終的に 10000 を出力します。次に、リソースを共有します。リソースを一度だけ解放する方法は? ----参照カウント

shared_ptr が提供するインターフェースを使用して、use_count()現在同じリソースを管理しているスマート ポインターの数を確認できます。

void test()
{
    
    
	shared_ptr<A> sp(new A);
	cout << sp.use_count() << endl;//1
	shared_ptr<A> sp2(sp);
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	shared_ptr<A> sp3(new A);
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	cout << sp3.use_count() << endl;//1
	sp3 = sp;
	sp3 = sp2;
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	cout << sp3.use_count() << endl;//2
}

実行中のスクリーンショット: 途中でデストラクタの呼び出しがある理由は、sp3 が sp を指している場合、sp3 の参照カウントが 0 であり、リソースを解放するためにデストラクタが呼び出されるためです。現時点では、sp によって作成されたリソースには、
ここに画像の説明を挿入
ダイアグラムを管理するための 3 つのスマート ポインターがあります。
ここに画像の説明を挿入
実装時には、各スマート ポインターが独自のカウンターを持つのではなく、リソースが 1 つのカウンターのみに対応するようにする必要があります。したがって、リソースとカウンターを一緒にバインドできます. このとき、同じリソースを指すスマート ポインターは同じカウンター メンバー
変数にアクセスします: メンバー変数には、リソース ポインターの変数 _ptr と変数 _ptr の 2 つの変数が必要です。リソース ポインターの. カウンター変数 _countPtr, それらはすべてポインター型の変数
コピー コンストラクターです: コピー コンストラクターでは、現在のオブジェクトのポインターは、コピーされるオブジェクトのリソースを指している必要があり、そのカウンターもコピーする必要があります。 、そして最後にカウンターはPerform ++
代入演算子のオーバーロードである必要があります:2つのオブジェクトが等しいかどうかを判断することはできませんが、2つのオブジェクトのリソースが異なる場合にのみ値を割り当てる必要があります. 割り当てでは、最初に現在のオブジェクトのカウンターを指定します。これが 0 の場合、現在のオブジェクトのリソースは現在のオブジェクトによってのみ管理されており、リソースを解放する必要があることを意味します。次に、コピーするオブジェクトのリソースへの現在のオブジェクト ポインターを変更し、そのカウンターをコピーします。最後に、カウンターは ++ 操作する必要があります
デストラクタ: 現在のオブジェクトのリソースのカウンターを判断するには、最初に – 操作を実行し、次にカウンターが 0 かどうかを判断し、0 の場合はリソースを解放します。が 0 でない場合、何も行われません

template<class T>
class Shared_ptr
{
    
    
public:
	Shared_ptr(T* ptr)
		:_ptr(ptr)
		, _countPtr(new size_t(1))//初始化为1
	{
    
    }
	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _countPtr(sp._countPtr)
	{
    
    
		//计数器累加
		++(*_countPtr);
	}
	Shared_ptr<T> operator=(const Shared_ptr<T>& sp)
	{
    
    
		if (_ptr != sp._ptr)
		{
    
    
			//本身计数器自减
			//计数器为0,则当前对象需要释放资源
			if (--(*_countPtr) == 0)
			{
    
    
				delete _ptr;
				delete _countPtr;
			}

			_ptr = sp._ptr;
			_countPtr = sp._countPtr;

			++(*_countPtr);
		}
		return *this;
	}

	~Shared_ptr()
	{
    
    
		//计数器自减
		if (--(*_countPtr) == 0)
		{
    
    
			delete _ptr;
			delete _countPtr;
			_ptr = nullptr;
			_countPtr = nullptr;
		}
	}
	T& operator*()
	{
    
    
		return *_ptr;
	}
	T* operator->()
	{
    
    
		return _ptr;
	}
private:
	T* _ptr;
	size_t* _countPtr;//计数器指针
};

実際に実装した shared_ptr スマート ポインターには、マルチスレッド シナリオでスレッド セーフの問題はありません。参照カウンター ポインターは共有変数であり、複数のスレッドが変更されると、カウンターの混乱が発生します。リソースが早期に解放されるか、メモリ リークが発生する
コードを見てみましょう。安全であれば、最後のデストラクタは 1 回だけ呼び出す必要があります。

void fun(const Shared_ptr<A>& sp, int n)
{
    
    
	for (int i = 0; i < n; ++i)
		Shared_ptr<A> copy(sp);//创建copy智能指针
}

void test()
{
    
    
	Shared_ptr<A> sp(new A);
	int n = 100000;
	thread t1(fun, ref(sp), n);
	thread t2(fun, ref(sp), n);
	t1.join();
	t2.join();
}

実行結果 1 : オブジェクトのデストラクタが呼び出されていないため、この時点でメモリ リークが発生していることがわかりました 実行
ここに画像の説明を挿入
結果 2 : デストラクタを 2 回呼び出すということは、リソースが 2 回解放されることを意味します。
ここに画像の説明を挿入
クラスでカウンターの値を取得するためのインターフェースを提供できます

	size_t getCount()
	{
    
    
		return *_countPtr;
	}

次に、コードを実行してカウンターの値を取得し、カウンターの値が 0 ではないことを確認します。そのため、デストラクタは呼び出されません。

ここに画像の説明を挿入
したがって、カウンターが変更された場所でロック保護を実行できます。また、このロックをグローバル変数ロックにすることはできず、リソースに影響を与えることはできません. そうしないと、1 つのリソースがロックされて変更されると、別のリソースが影響を受け、コードの実行効率に影響します. カウンターごとに個別のロックを提供する必要があります。
ここでは、++ 操作と – 操作の両方がカプセル化されています。

template<class T>
class Shared_ptr
{
    
    
public:
	Shared_ptr(T* ptr)
		:_ptr(ptr)
		, _countPtr(new size_t(1))//初始化为1
		, _mtx(new mutex)
	{
    
    }
	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _countPtr(sp._countPtr)
		,_mtx(sp._mtx)
	{
    
    
		//计数器累加
		//++(*_countPtr);
		addCount();
	}
	Shared_ptr<T> operator=(const Shared_ptr<T>& sp)
	{
    
    
		if (_ptr != sp._ptr)
		{
    
    
			//本身计数器自减
			//计数器为0,则当前对象需要释放资源
			//if (--(*_countPtr) == 0)
			if (subCount() == 0)
			{
    
    
				delete _ptr;
				delete _countPtr;
				delete _mtx;
			}

			_ptr = sp._ptr;
			_countPtr = sp._countPtr;

			addCount();
		}
		return *this;
	}

	~Shared_ptr()
	{
    
    
		//计数器自减
		if (subCount() == 0)
		{
    
    
			delete _ptr;
			delete _countPtr;
			delete _mtx;
			_ptr = nullptr;
			_countPtr = nullptr;
			_mtx = nullptr;
		}
	}
	T& operator*()
	{
    
    
		return *_ptr;
	}
	T* operator->()
	{
    
    
		return _ptr;
	}

	size_t getCount()
	{
    
    
		return *_countPtr;
	}

	size_t addCount()
	{
    
    
		_mtx->lock();
		++(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}

	size_t subCount()
	{
    
    
		_mtx->lock();
		--(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}
private:
	T* _ptr;
	size_t* _countPtr;//计数器指针
	mutex* _mtx;
};

実行結果: マルチスレッドのシナリオでは、正常にリリースできることもわかりました
ここに画像の説明を挿入

循環参照の問題

shared_ptr には実際にはいくつかの小さな問題、つまり循環参照の問題があります.
最初に次のコードを見てみましょう.

struct ListNode
{
    
    
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
	int _data;

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

void test()
{
    
    
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	n1->_next = n2;
	n2->_prev = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
}

実行結果: リソースが解放されておらず、カウンターも増加していることがわかりました
ここに画像の説明を挿入

ここに画像の説明を挿入
ここに画像の説明を挿入

C++11 では、特にこの問題を解決するために、新しいスマート ポインター waek_ptr が導入されました。これは、ウィーク ポインターと呼ばれます。割り当てまたはコピーの場合、カウンターは ++ を実行しません。破壊中に実際のリソースが解放されることはありません。Waek_ptr は単体では使えず、最大の役割は shared_ptr の循環参照の問題を解決することです。

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

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

void test()
{
    
    
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	n1->_next = n2;
	n2->_prev = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
}

実行結果:
ここに画像の説明を挿入
独自の shared_ptr では、リソースを解放するときにリソースを解放するためにのみ delete を使用します. スペースを申請する方法では、単に new を使用してスペースを申請するのではなく、malloc を使用してスペースを申請することもあります.現時点では無料でリリースする必要があります。そのため、スマート ポインターにデリータも追加する必要があります。

void test()
{
    
    
	Shared_ptr<A> sp(new A[100]);//调用析构会报错
}

Deletor は、主にファンクターを介して実装できます

template<class T>
struct DeleteDel
{
    
    
	void operator()(T* ptr)
	{
    
    
		delete ptr;
	}
};

template<class T>
struct FreeDel
{
    
    
	void operator()(T* ptr)
	{
    
    
		free(ptr);
	}
};

template<class T>
struct DeleteArrDel
{
    
    
	void operator()(T* ptr)
	{
    
    
		delete[] ptr;
	}
};
template<class T, class Del = DeleteDel<T>>
class Shared_ptr
{
    
    
public:
	Shared_ptr(T* ptr)
		:_ptr(ptr)
		, _countPtr(new size_t(1))//初始化为1
		, _mtx(new mutex)
	{
    
    }
	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _countPtr(sp._countPtr)
		,_mtx(sp._mtx)
	{
    
    
		//计数器累加
		//++(*_countPtr);
		addCount();
	}
	Shared_ptr<T> operator=(const Shared_ptr<T>& sp)
	{
    
    
		if (_ptr != sp._ptr)
		{
    
    
			//本身计数器自减
			//计数器为0,则当前对象需要释放资源
			//if (--(*_countPtr) == 0)
			if (subCount() == 0)
			{
    
    
				//delete _ptr;
				//通过删除器来释放空间
				_del(_ptr);
				delete _countPtr;
				delete _mtx;
			}

			_ptr = sp._ptr;
			_countPtr = sp._countPtr;

			addCount();
		}
		return *this;
	}

	~Shared_ptr()
	{
    
    
		//计数器自减
		if (subCount() == 0)
		{
    
    
			//delete _ptr;
			//通过删除器来释放空间
			_del(_ptr);
			delete _countPtr;
			delete _mtx;
			_ptr = nullptr;
			_countPtr = nullptr;
			_mtx = nullptr;
		}
	}
	T& operator*()
	{
    
    
		return *_ptr;
	}
	T* operator->()
	{
    
    
		return _ptr;
	}

	size_t getCount()
	{
    
    
		return *_countPtr;
	}

	size_t addCount()
	{
    
    
		_mtx->lock();
		++(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}

	size_t subCount()
	{
    
    
		_mtx->lock();
		--(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}
private:
	T* _ptr;
	size_t* _countPtr;//计数器指针
	mutex* _mtx;
	Del _del;
};

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/qq_44443986/article/details/117415950