C++11 - スマート ポインター
序文:
Vue框架:
プロジェクトからVue の
OJ算法系列:
魔法のトリックを学ぶ - アルゴリズムの詳細な説明
Linux操作系统:
Fenghou Qimen - linux
通常のポインタ:
セキュリティリスク:
その他の関数の例外:
- 例外のある次の関数を見てください。
int div(){
int a, b;
cin >>a >>b;
if(b == 0)
throw invalid_argument("除0错误");
return a/b;
}
void func(){
int *p = new int;
cout<<div() <<endl;
delete p;
}
int main(){
try{
func();
}catch(const exception &e){
cout<< e.what() <<endl;
}
return 0;
}
- 例外ソース:
div() 関数が異常の場合、try catch メカニズムが設定されていないため、要求された p ポインターを削除によって解放できません。 - 処理戦略: try catch
int div(){
int a, b;
cin >>a >>b;
if(b == 0)
throw invalid_argument("除0错误");
return a/b;
}
void func(){
int *p = new int;
try{
cout<<div() <<endl;
}catch(const exception &e){
cout<< e.what() <<endl;
}
delete p;
}
int main(){
try{
func();
}
catch(const exception &e){
cout<< e.what() <<endl;
}
return 0;
}
新しい関数の例外:
-
ポインター用のスペースを複数回申請し、均等に解放します。
1 つのアプリケーションでエラーが発生すると、それまでのすべてのアプリケーションのリソースを解放できません。
int main(){
int *p1 = new int;
int *p2 = new int;
int *p3 = new int;
delete p1;
delete p2;
delete p3;
return 0;
}
- 処理戦略:
- 試す + キャッチする
- 初期値を nullptr に設定すると、アプリケーションが失敗した後も値は nullptr のままになります。
int main(){
int *p1 = nullptr;
int *p2 = nullptr;
int *p3 = nullptr;
try{
int *p1 = new int;
int *p2 = new int;
int *p3 = new int;
}catch(...){
if(p2 == nullptr)
delete p1;
if(p3 == nullptr){
delete p1;
delete p2;
}
}
delete p1;
delete p2;
delete p3;
return 0;
}
スマートポインター:
RAII 原則:
-
RAII:リソース取得は初期化です
- オブジェクトのライフサイクルによるプログラムリソースの制御
- オブジェクトの構築時にリソースを取得する
- オブジェクトが破壊されたときにリソースを解放する
- 実際、リソースの管理責任はオブジェクトに引き継がれます。
-
利点:
- リソースを明示的に解放する必要はありません
- オブジェクトが必要とするリソースは、オブジェクトの存続期間中常に有効です。
-
物質:
メンバ変数がテンプレート ポインタであるクラス
テンプレート ポインタは、新しい関数の戻りポインタを指します。
無効化後にリソースが破棄された場合、テンプレートポインタが指すリソースを解放します。
スマート_ptr:
- デストラクターのバグのあるバージョン:
template <class T>
class smart_ptr{
private:
T *ptr;
public:
smart_ptr(T *_ptr){
ptr = _ptr;
}
T& operator*(){
return *ptr;
}
T* operator->(){
return ptr;
}
//有bug的析构函数
~smart_ptr(){
cout<<ptr <<" " <<*ptr <<endl;
delete ptr;
}
}
int main(){
smart_ptr<int> sp1(new int);
smart_ptr<int> sp2(new int);
smart_ptr<int> sp3(new int);
//析构函数bug体现:同一空间被析构两次,肯定报错
int *p = new int;
smart_ptr<int> sp4 = p;
smart_ptr<int> sp5 = p;
cout<<div() <<endl;
return 0;
}
- 明らかに、このバージョンの SmartPtr には欠点はありませんが、デストラクターの脆弱性があり、まったく使用できません。
- 以下では、SmartPtr() の複数の破壊を防ぐための C++98 および C++11 のいくつかの異なる設計を紹介します。
auto_ptr:
経営権の譲渡:
- 出現年齢:c++98
- 同じリソースを複数回削除する場合の解決策:
リソースへのポインターがもう 1 つある場合の 管理権の譲渡- リソースの管理を最新のポインタに引き継ぎます。
- 同時に、元のポインターはすべて null を指します。
- コード:
template <class T>
class auto_ptr{
private:
T *ptr;
public:
auto_ptr(T *_ptr){
ptr = _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;
}
T& operator*(){
return *ptr;
}
T* operator->(){
return ptr;
}
~auto_ptr(){
delete ptr;
}
};
アドバンテージ:
-
アドバンテージ:
二重破壊の問題を素直に解決する
欠点:
-
デメリット1:
新しい auto_ptr が到着すると、古い auto_ptr が上書きされます。
auto_ptr に慣れていない学生はこのような間違いを犯す可能性があります
int main(){
auto_ptr<int> sp1(new int);
auto_ptr<int> sp2(sp1);
*sp2 = 10;
cout<< *sp2 <<endl;
cout<< *sp1 <<endl;
return 0;
}
-
デメリット2:
管理権の譲渡は auto_ptr オブジェクト間でのみ存在します。
ポインターから初期化された auto_ptr オブジェクトが 2 つある場合でも、二重破壊の問題が発生します。
int main(){
int *p = new int(10);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2(p);
return 0;
}
unique_str:
- 登場年:ブーストライブラリ、同時期の3つのスマートポインタはscoped_ptr/shared_ptr/weak_ptr
- C++11 のブーストライブラリのリファレンス製品: unique_ptr /shared_ptr /weak_ptr
- 同じリソースを複数回削除するための解決策: コピー防止
各ポインターがスペースを空けた後、ポインターをカプセル化できる unique_ptr/scoped_ptr オブジェクトは 1 つだけです。
コピー防止:
- コピー防止機能:
- 方法 1: キーワードを削除する
- 方法 2: プライベート + 空の実装 (宣言のみで実装はしない)
- コード:
template <class T>
class unique_ptr{
private:
T *ptr;
//c++11中delete关键字屏蔽函数
unique_ptr(unique_ptr<T> const &) = delete;
unique_ptr& operator=(unique_ptr<T> const &) = delete;
//c++98中私有 + 只声明不实现
unique_ptr(unique_ptr<T> const &);
unique_ptr& operator=(unique_ptr<T> const &);
public:
unique_ptr(T *_ptr){
ptr = _ptr;
}
~unique_ptr(){
delete ptr;
}
void Show(){
cout<<*ptr <<endl;
}
};
欠点:
-
理論上、ポインターごとに存在できる unique_ptr オブジェクトは 1 つだけです
-
unique_ptr オブジェクトのコピー割り当てを使用しない場合、
代わりに、2 つの unique_ptr オブジェクトがポインターを使用して直接初期化される場合でも、二重破壊が発生します。
int *p = new int(10);
unique_ptr<int> uq1(p);
unique_ptr<int> uq2(p);
共有_ptr:
- 原理:
- 同じリソースを管理するオブジェクトの数を記録する
- 各オブジェクトが破壊されたときにカウンター - -
- 各オブジェクト構築時のカウンタ ++
- 最後に破棄されたオブジェクトがリソースを解放します。
静的参照カウンタ:
コード:
- コード:
template <class T>
class shared_ptr{
private:
static int refCount;
T *ptr;
public:
shared_ptr(T *_ptr){
refCount = 0;
ptr = _ptr;
}
shared_ptr(auto_ptr &ap){
refCount++;
ptr = ap.ptr;
}
~shared_ptr(){
refCount--;
if(refCount == 0 && ptr){
delete ptr;
}
}
};
int main(){
shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(sp1);
shared_ptr<int> sp4(new int);
return 0;
}
脆弱性:
カウンターの混乱の問題:
-
各リソースは独自のカウンターを独立して使用する必要があります
すべてのリソースが同じ参照カウンタを使用する場合、結果は次のようになります。
-
初期化時:
- sp1 が int* で初期化される場合、カウンター refCount == 0
- sp2 が sp1 で初期化されると、カウンター refCount == 1
- sp3 が sp1 で初期化されると、カウンター refCount == 2
- sp4 が int* で初期化される場合、カウンター refCount == 0
-
破壊するとき:
sp1、sp2、sp3 はすべてメモリの同じブロックを破壊し、複数の破壊例外を引き起こします
ダイレクトポインタ構築の問題:
- 引き続きルーチンには従わず、ポインターを直接使用して 2 つのオブジェクトを構築します。
int *p = new int(10);
shared_ptr<int> sp1(p); //静态计数器refCount == 0
shared_ptr<int> sp2(p); //静态计数器refCount == 0
/*
析构时直接双重析构,异常
*/
-
静的参照カウンター自体にはストレージの脆弱性があり、ポインターからオブジェクトを直接構築する問題を解決できません。
動的参照カウンターが 2 つの問題を同時に解決できるかどうか見てみましょう。
動的参照カウンター:
コード:
- コード:
template <class T>
class shared_ptr{
private:
T *ptr;
int *refCount; //动态引用计数器
public:
shared_ptr(T *_ptr){
ptr = _ptr;
refCount = (new int(1));
//从这步开始已经决定了智能指针只能走对象拷贝路线,不能走指针直接构造路线
}
shared_ptr(const shared_ptr<T> &sp){
ptr = sp.ptr;
refCount = sp.refCount;
(*refCount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T> &sp){
if(ptr != sp.ptr){
ptr = sp.ptr;
refCount = sp.refCount;
(*refCount)++;
}
}
~shared_ptr(){
if(--(*refCount) == 0 && ptr){
delete ptr;
delete refCount;
}
}
};
int main(){
shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(sp1);
shared_ptr<int> sp4(new int);
return 0;
}
アドバンテージ:
-
各リソースは独自のカウンターを独立して使用します
異なるリソースカウンターは相互に干渉しません
欠点:
オブジェクト割り当てコピー時のカウンタ エラー:
- 2 つのポインターが異なるオブジェクトを指している場合、割り当てコピーが発生します。
class shared_ptr{
shared_ptr<T>& operator=(const shared_ptr<T> &sp){
if(ptr != sp.ptr){
ptr = sp.ptr;
refCount = sp.refCount;
(*refCount)++;
}
}
};
- リソース参照の数は増加するだけで減少しないため、最終的には破棄と解放に失敗する可能性があります。
- 対策: オブジェクトをコピーして構築するたびに、最初に元のリソースの参照番号を – にし、次に現在のリソースの参照番号を ++ にします。
class shared_ptr{
shared_ptr<T>& operator=(const shared_ptr<T> &sp){
if(ptr != sp.ptr){
//原资源引用数--
if(--(*refCount) == 0){
delete ptr;
delete refCount;
}
//先资源引用数++
ptr = sp.ptr;
refCount = sp.refCount;
(*refCount)++;
}
}
};
マルチスレッド下でのセキュリティ問題に対処します。
- 複数のスレッドに同じリソースへのスマート ポインターが同時に含まれている場合、次の状況が発生する可能性があります。
- カウンタ ++ の追加は少なくなります:
1. スマート ポインタはメイン スレッドで作成されます (refCount = 1);
2. サブスレッド 1 はメイン スレッドのスマート ポインタ オブジェクトをコピーしますが、refCount はまだ完了していません (++ は分割されています) 3.メインスレッドの
スマート ポインタ オブジェクトをサブスレッド 2 にコピーし、refCount++ が完了します。
4. サブスレッド 1 の refCount++ が完了します
。 5. このとき、同じリソースへの参照カウンタは3、ただし 2 だけ - Counter - - Decrease:
1. すべてのスレッドにスマート ポインタ オブジェクトが 2 つだけある場合:
2. スマート ポインタ オブジェクトは子スレッド 1 で使い果たされ、破棄され始めますが、refCount – が完了していません (– 3 つのアトミックに分割されています)手順)
3. スマート ポインタ オブジェクトはサブスレッド 2 でも使用され、デストラクタが開始され、refCount-- が完了します
。ただし、この時点では refCount は 0 ではありません。
5. 子スレッド 1 が refCount– を完了し、誰もリソースを削除しないため、メモリ リークが発生します。
- カウンタ ++ の追加は少なくなります:
- スレッドの安全対策:
- ロック
- ロックも refCount の原則から学び、ポインターを使用して「1 人あたり 1 つのロック」を完了する必要があります。
真のスマート ポインター:
コード:
-
動的参照カウントの 2 つの大きな欠点を吸収した後、
最終的に、基本的にセキュリティ上の問題のないスマート ポインター クラスを作成できます。
#include <iostream>
#include <mutex>
using namespace std;
template <class T>
class SharedPtr{
private:
T *ptr;
int *refCount;
mutex *mtx;
private:
void AddRefCount(){
mtx.lock();
*refCount++;
mtx.unlock();
}
void SubRefCount(){
bool flag = 0;
mtx.lock();
if (--(*refCount) == 0){
delete ptr;
delete refCount;
flag = true;
}
mtx.unlock();
if(flag == 1)
delete mtx;
}
public:
SharedPtr(T *_ptr){
ptr = _ptr;
refCount = new int(0);
mtx = new mutex;
}
//默认采用拷贝构造的对象暂无ptr/refCount/mtx
SharedPtr(SharedPtr<T> &sp){
ptr = sp.ptr;
refCount = sp.refCount;
mtx = sp.mtx;
AddRefCount();
}
//默认采用赋值构造的对象已有ptr/refCount/mtx
SharedPtr<T>& operator=(SharedPtr<T> &sp){
if(ptr != sp.ptr){
SubRefCount();
ptr = sp.ptr;
refCount = sp.refCount;
mtx = sp.mtx;
AddRefCount();
}
}
~ShardPtr(){
SubRefCount();
}
};
アドバンテージ:
-
このクラスの基本的な利点: ポインタが指すリソースは、使用されないときに自動的に破棄され、解放されます。
-
複数のスマート ポインター オブジェクトがリソースを共有します。
各リソースは参照カウンタを独立して使用します
直接ポインタ構築 / コピー構築 / 代入構築 / 破壊、複数の破壊はありません
-
スレッド セーフ: ++/less - - 参照カウンターは存在しません
欠点:
- 複数のスマート ポインター オブジェクトがポインターを直接使用してオブジェクトを構築する場合でも、複数のデストラクター例外が発生します。
int *p = new int(10);
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p);
shared_ptr<int> sp3(p);
- コピー構築で最初に元のリソース参照カウンタを減らす必要があるかどうかについては、ユーザーに別途説明する必要があります。
- コピー構築が空のオブジェクトに対してのみ使用できる場合、元のリソース参照カウンターをデクリメントする必要はありません。
- コピー構築がすでに割り当てられているオブジェクトに対してのみ使用できる場合は、元のリソース参照カウンターを減らす必要があります。
脆弱性: 循環参照
- 破棄プロセスを分析するには、以下のリンク リスト ノードのスマート ポインタを確認してください。
struct ListNode{
int data;
shared_ptr<ListNode> prev;
shared_ptr<LIstNode> next;
~ListNode(){
cout<<"~ListNode()"<<endl;
}
}
int main(){
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->next = node2;
node2->next = node1;
return 0;
}
-
ノードの破壊には 3 つの部分が必要です
- 実行日
- プリカーサーポインター
- 後継ポインタ
-
現在の 2 つのリソースの参照カウンターのステータス:
-
ノード 1 を解放するには、リソース参照カウンター == 0 が必要です
ノード 1 が refCount– を完了すると、refCount==1
refCount を継続したい場合は、node2 の次のノードを解放する必要があります。
ノード 2 の次を解放するには、ノード 2 を解放する必要があります
-
ノード 2 を解放するには、リソース参照カウンター == 0 が必要です
ノード 2 が refCount– を完了した後、refCount==1
refCount を続行するには、node1 の前を解放する必要があります。
ノード 1 の前を解放するには、ノード 1 を解放する必要があります
-
ロックセットロックと同様のデッドロック状況があることがわかりますが、ここでは循環参照と呼ばれます
C++11 の循環参照を解決するweak_ptr<>を見てみましょう。
弱い_ptr:
原理:
- 循環参照の根本原因:
node1->next = node2;
node2->next = node1;
//各自资源引用计数器数目 +1 了
- 循環参照を避けるためのweak_ptrの対策:
- 参照されるオブジェクトがインクリメントされても、リソース カウンターはインクリメントされません
- 参照されたオブジェクトを破棄する場合、二重破棄は発生しません。
使用:
- 自分たちで実装を手動でシミュレートすることはありません: #include <memory>
struct ListNode{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
return 0;
}
カスタム削除者:
バックグラウンド:
-
完全に機能するshared_ptrであっても、特別なシナリオにおけるweak_ptrであっても、
スマートポインタと呼ばれるだけあって、多くの種類のポインタを受け取ることができます
ただし、さまざまな種類のポインタが指すリソースは、さまざまな方法で解放されます。
- 削除するにはnew -> deleteで作成されたスペースへのポインタ
- new[] -> delete[] で開いたスペースへのポインタを削除します。
- mallocで開いた空間へのポインタ -> 解放
- fopen() でオープンされたファイルへのポインタ -> fclose() でクローズされた
-
したがって、単にこれらのスマート STL クラスにポインターを渡すときは、カスタム デリーターと呼ばれるポインターを削除する方法も渡す必要があります。
デリーターの種類:
-
delete 関数は本質的に呼び出し可能なオブジェクトです。
- 関数名/関数ポインタ
- ファンクタークラスオブジェクト
- ラムダ式
-
new delete と new[] delete[] の違いを思い出してください。
- newで作成した空間はすべてストレージ内容で、deleteは型サイズ+メモリの先頭アドレスに従って削除可能
- new[]からの空間の先頭には要素数が格納され、deleteは要素数+型サイズ+メモリ先頭のアドレスに従って削除します。
- 本質的に、2 つのキーワード グループは一貫性のない方法でメモリ空間を観察しており、1 ビットの違いでも大きな違いを生みます。
- しかし、現在では多くのコンパイラーが最適化を行っており、new[] の数値を含むスペースも delete で削除して解放できるようになりました。
シミュレーションの実装:
- 受信したデリーター関数とその型はクラスに保存されるため、関数パラメーターの代わりに使用できるのはテンプレート ファンクター クラスのみです。
template <class T>
class default_delete{
public:
void operator()(const T*ptr){
cout<<"delete:"<<ptr<<endl;
delete ptr;
}
};
template <class T, class D = default_delete<T>>
class del_ptr{
private:
T *ptr;
public:
unique_ptr(T *_ptr){
ptr = _ptr;
}
~unique_ptr(){
if(ptr){
D del;
del(ptr);
}
}
};
struct DeleteArray{
void operator()(A* ptr){
cout<< "delete[] : "<<ptr <<endl;
delete[] ptr;
}
};
struct DeleteFile{
void operator()(FILE* ptr){
cout<< "fclose[] : "<<ptr <<endl;
fclose(ptr);
}
};
int main(){
del_ptr<A> sp1(new A); //默认删除器
del_ptr<A, DeleteArray> sp2(new A[10]);
del_ptr<A, DeleteFile> sp3(fopen("test.txt", "r"));
}
STL のデリーター:
- STL 実装はより複雑で、以下をサポートします。
- デリーター関数に渡される関数の引数
- デリーター テンプレート ファンクター クラスをテンプレート クラス内に保存します
テンプレートはファンクター クラスを渡します。
- テンプレートファンクタークラス:
struct DeleteArray{
void operator()(A* ptr){
cout<< "delete[] : "<<ptr <<endl;
delete[] ptr;
}
};
int main(){
unique_ptr<A> sp1(new A);
unique_ptr<A, DeleteArray> sp1(new A);
return 0;
}
パラメーターを削除関数に渡します。
- パラメーターを削除関数に渡します。
int main(){
unique_ptr<A> sp1(new A[10], [](A* p){
delete[] p;
},);
unique_ptr<A> sp2(fopen("test.txt","r"), [](FILE *p){
fclose(p);
});
}