初心者向けのスマートポインター

この短い記事は主に、スマートポインターについては聞いたがスマートポインターを使用することを恐れていた、または新しい削除を実行するのにうんざりしていた初心者のC ++プログラマーを対象としています。



UPD:この記事は、C ++ 11がまだそれほど普及していないときに書かれました。



したがって、C ++プログラマは、メモリを解放する必要があることを知っています。 常に望ましい。 そして、newがどこかに書き込まれた場合、対応する削除が必要であることを彼らは知っています。 ただし、たとえば次のエラーが発生すると、メモリを使用した手動操作が困難になります。



プログラムが24時間365日動作しない場合、またはそれを呼び出すコードがループ内にない場合、原則として、リークはそれほど重大ではありません。 nullポインターを間接参照すると、セグメンテーション違反が発生することが保証されます。nullになった場合にのみ検出されます(意味はわかります)。 繰り返し削除すると、何でも起こります。 通常(常にそうとは限りませんが)、メモリブロックが割り当てられている場合、その隣のどこかに割り当てられたメモリの量を決定する値があります。 詳細は実装に依存しますが、あるアドレスから1 kbのメモリを割り当てたとしましょう。 次に、1024という数値が前のアドレスに格納されるため、deleteを呼び出すときに、1024バイトのメモリを削除することが可能になります。 最初に削除するとすべてが正常になり、2回目は間違ったデータを消去します。 これをすべて回避するため、またはこの種のエラーの可能性を減らすために、スマートポインターが考案されました。



はじめに



RAIIと呼ばれるローカルオブジェクトを介したリソース管理手法があります。 つまり、リソースを受け取ると、コンストラクターで初期化され、関数でリソースを操作した後、デストラクターで正しく解放されます。 リソースは、ファイル、ネットワーク接続、この場合はメモリブロックなど、何でもかまいません。 以下に簡単な例を示します。

class VideoBuffer { int *myPixels; public: VideoBuffer() { myPixels = new int[640 * 480]; } void makeFrame() { /*    */ } ~VideoBuffer() { delete [] myPixels; } }; int game() { VideoBuffer screen; screen.makeFrame(); }
      
      





これは便利です。画面オブジェクトのデストラクタが呼び出され、カプセル化されたピクセル配列が解放されるため、関数を終了するときにバッファを解放する必要はありません。 もちろん、次のように書くことができます。

 int game() { int *myPixels = new int[640 * 480]; //  delete [] myPixels; }
      
      





原則として、違いはありませんが、そのようなコードを想像してください。

 int game() { int *myPixels = new int[640 * 480]; //  if (someCondition) return 1; //   if (someCondition) return 4; //  delete [] myPixels; }
      
      





関数出口の各ブランチでdelete []を記述するか、追加の初期化解除関数を呼び出す必要があります。 そして、メモリ割り当てがたくさんある場合、またはそれらは関数の異なる部分で発生しますか? これらすべてを追跡することはますます難しくなります。 関数の途中で例外をスローした場合も、同様の状況が発生します。スタック上のオブジェクトは確実に破棄されますが、問題が山積しているため、問題は未解決のままです。

OK、RAIIを使用して、コンストラクターでメモリを初期化し、デストラクタで解放します。 そして、クラスのフィールドを動的メモリのセクションへのポインターにしましょう:

 class Foo { int *data1; double *data2; char *data3; public: Foo() { data1 = new int(5); ... } ~Foo() { delete data1; ... } }
      
      





ここで、フィールドが3ではなく30であると想像してください。これは、デストラクタですべてのフィールドに対してdeleteを呼び出す必要があることを意味します。 そして、急いで新しいフィールドを追加しても、デストラクタでそれを殺すのを忘れると、結果はマイナスになります。 結果は、メモリ割り当て/割り当て解除操作でロードされたクラスであり、すべてが正しく削除されたかどうかは不明です。

したがって、スマートポインターを使用することをお勧めします。これらは、任意のタイプのメモリの動的に割り当てられたセクションへのポインターを格納するオブジェクトです。 さらに、スコープを離れると自動的にメモリをクリアします。

まず、C ++でどのように見えるかを見てみましょう。次に、いくつかの一般的なタイプのスマートポインターの概要に進みます。



最も単純なスマートポインター



 //     template <typename T> class smart_pointer { T *m_obj; public: //       smart_pointer(T *obj) : m_obj(obj) { } //          ~smart_pointer() { delete m_obj; } //  < // .      T  "" T* operator->() { return m_obj; } //             // ,    T& operator* () { return *m_obj; } } int test { //  myClass     smart_pointer<MyClass> pMyClass(new MyClass(/*params*/); //     MyClass   pMyClass->something(); // ,          ostream //          , //       std::cout << *pMyClass << std::endl; //      MyClass   }
      
      





スマートポインターに欠点がないわけではないことは明らかです(たとえば、配列に配列を格納する方法など)。ただし、RAIIイディオムは完全に実装されています。 通常のポインタと同じように動作します(オーバーロードされた演算子のおかげです)。メモリの解放について心配する必要はありません。すべてが自動的に行われます。 オプションで、オーバーロードされた演算子にconstを追加して、参照されるデータが一定であることを保証できます。

ここで、このようなポインターを使用するときに特定の利点が得られることを理解している場合は、特定の実装を検討してください。 このアイデアが気に入らない場合は、とにかく、あなたの小さなプログラムのいくつかでそれらを使用してみてください、あなたはそれを気に入るはずです。

そのため、スマートポインター:





ブースト:: scoped_ptr



ブーストライブラリにあります。

実装はシンプルでわかりやすく、いくつかの例外を除き、ほとんど同じです。このポインターはコピーできません(つまり、プライベートコピーコンストラクターと代入演算子があります)。 例で説明します。

 #include <boost/scoped_ptr.hpp> int test() { boost::scoped_ptr<int> p1(new int(6)); boost::scoped_ptr<int> p2(new int(1)); p1 = p2; // ! }
      
      





割り当てが許可されていれば、p1とp2は同じメモリ領域を指しているのは理解できます。 関数を終了すると、両方が削除されます。 どうなるの? 誰も知らない。 したがって、このポインターを関数に転送することはできません。

次に、なぜそれが必要ですか? リソースの正しいクリーニングに関する頭痛からあなたを救うために、関数の最初に動的に割り当てられ、最後に削除されるデータのラッパーポインターとして使用することをお勧めします。

詳細な説明はこちら



std :: auto_ptr



さらに、以前のバージョンのわずかに改善されたバージョンは、標準ライブラリにあります(ただし、C ++ 11では非推奨のようです)。 代入演算子とコピーコンストラクターがありますが、それらは多少異常です。

私が説明する:

 #include <memory> int test() { std::auto_ptr<MyObject> p1(new MyObject); std::auto_ptr<MyObject> p2; p2 = p1; }
      
      





これで、p2に割り当てると、MyObject(p1用に作成した)へのポインターができ、p1には何もありません 。 つまり、p1はゼロにリセットされます。 これは、いわゆる動きのセマンティクスです。 ところで、コピー演算子は同じことをします。

なぜこれが必要なのですか? たとえば、たとえば、ある種のオブジェクトを作成する関数があるとします。

 std::auto_ptr<MyObject> giveMeMyObject();
      
      





これは、関数がMyObject型の新しいオブジェクトを作成し、自由に使えることを意味します。 この関数自体がクラスのメンバー(ファクトリーなど)である場合、より明確になります。このクラス(ファクトリー)が新しいオブジェクトへの別のポインターを格納しないことは確かです。 オブジェクトとその1つのポインター。

このような異常なセマンティクスのため、auto_ptrはSTLコンテナでは使用できません。 しかし、shared_ptrがあります。



std :: shared_ptr(C ++ 11)



参照カウント付きのスマートポインター。 これはどういう意味ですか。 これは、オブジェクトを参照するポインターの数を格納する特定の変数がどこかにあることを意味します。 この変数がゼロに等しくなると、オブジェクトは破棄されます。 カウンターは、コピー演算子または割り当て演算子のいずれかを呼び出すたびに増加します。 Shared_ptrにはboolキャスト演算子もあり、最終的にメモリの解放を心配することなくポインターの通常の構文を提供します。

 #include <memory> //  <tr1/memory>  ,    C++11 #include <iostream> int test() { std::shared_ptr<MyObject> p1(new MyObject); std::shared_ptr<MyObject> p2; p2 = p1; if (p2) std::cout << "Hello, world!\n"; }
      
      





これで、p2とp1の両方が同じオブジェクトを指し、参照カウンターが2になります。スコープを終了すると、カウンターがリセットされ、オブジェクトが破棄されます。 このポインターを関数に渡すことができます。

 int test(std::shared_ptr<MyObject> p1) { //  - }
      
      





ポインタを参照渡しすると、カウンタはインクリメントされないことに注意してください。 テスト関数の実行中にMyObjectが生きていることを確認する必要があります。



したがって、スマートポインターは優れていますが、欠点もあります。

まず、これは小さなオーバーヘッドですがこのような利便性のために、いくつかのプロセッササイクルが見つかると思います。

第二に、例えばボイラープレートです

 std::tr1::shared_ptr<MyNamespace::Object> ptr = std::tr1::shared_ptr<MyNamespace::Object>(new MyNamespace::Object(param1, param2, param3))
      
      





これは、次のような定義を使用して部分的に解決できます。

 #define sptr(T) std::tr1::shared_ptr<T>
      
      





またはtypedefを使用します。

第三に 、循環参照の問題があります。 記事を拡大しないように、ここでは検討しません。 Boost :: weak_ptr、boost :: intrusive_ptrおよび配列のポインターも未調査のままです。

ちなみに、スマートポインターは、「本物のプログラマーのためのC ++」という本で、Jeff Algerによってかなりよく説明されています。



All Articles