スマート ポインターとは何ですか? また、いつ使用する必要がありますか?

このチュートリアルでは、スマート ポインターと、C++ プログラムでスマート ポインターを使用する理由と方法について学習します。最初に、スマート ポインターとは何か、いつ使用する必要があるかについて説明します。このチュートリアルの主な前提条件は、ポインターに関する基本的な知識があることです。スマート ポインターの適用を理解する前に、通常のポインターの問題を理解しましょう。

通常のポインタまたは生のポインタの問題は何ですか?

new によって割り当てられたメモリが自動的に破棄されないことはご存知だと思いますが、delete を呼び出して手動で行う必要があります。 .必要なだけ保持できる利点があります。

「生の」C++ ポインターの問題は、オブジェクトが不要になったときにプログラマーがオブジェクトを明示的に破棄する必要があることです。割り当てられたメモリを解放し忘れたり、メモリを削除する前に例外が発生したりすると、メモリ リークが発生します。ご存知のように、プログラマーが割り当てられたメモリの割り当てを解除するのを忘れると、メモリ リークが発生します。

以下の C++ プログラムを参照してください。

#include <iostream>
using namespace std;


void fun()
{
    // Using a raw pointer -- not recommended.
    int* ptr = new int;

    /*
    Use ptr...
    */
}

int main()
{
    // Infinite Loop
    while (1)
    {
        fun();
    }

    return 0;
}

上記の関数 fun() 整数に割り当てられたメモリを指しているローカル生ポインタを作成しています。関数 fun() が終了すると、ローカル ポインター ptr スタック変数なので破棄されます。ただし、delete ptr を使用するのを忘れたため、指しているメモリは解放されません。; fun() の最後に。そのため、割り当てられたメモリに到達できなくなり、割り当てを解除できないため、メモリがリークします。

しかし、これはプログラマーのミスだと言うでしょう。削除を追加することを決して忘れません。私は常にクリーンでエラーのないコードを書いていますが、なぜスマート ポインターを使用する必要があるのでしょうか?そして、あなたは私に「ねえ、私のコードをチェックしてください」と尋ねました。ここでは、メモリを割り当て、使用後に適切に割り当てを解除しています。では、「スマート ポインターを使用する理由と、スマート ポインターの必要性」を教えてください。

#include <iostream>
using namespace std;


void fun()
{
    // Using a raw pointer -- not recommended.
    int* ptr = new int;

    /*
    Use ptr...
    .
    .
    .
    */
    delete ptr;
}

int main()
{
    // Infinite Loop
    while (1)
    {
        fun();
    }

    return 0;
}

あなたのコードを見た後、メモリを適切に割り当てて解放しているというあなたの言葉に同意します。また、コードは通常のシナリオで完全に機能します。

しかし、いくつかの実際的なシナリオについて考えてみてください。メモリの割り当てと解放の間の何らかの不正な操作により、何らかの例外が発生する可能性があります。この例外は、無効なメモリ位置へのアクセス、ゼロによる除算、または..などが原因である可能性があります

したがって、例外が発生した場合、または別のプログラマーが時期尚早の return ステートメントを統合して、メモリの割り当てと割り当て解除の間の別のバグを修正した場合。いずれの場合も、メモリが解放されるポイントに到達することはありません。上記のすべての問題に対する簡単な解決策は、スマート ポインターです。

これが、多くのプログラマーが生のポインターを嫌う理由です。多くの問題は、メモリ リーク、ダングリング ポインターなどの通常のポインターに関係しています。

スマート ポインターとは

スマート ポインターは、動的に割り当てられたメモリを処理するように設計された RAII モデル クラスです。スマート ポインターは、スマート ポインター オブジェクトがスコープ外に出たときに、割り当てられたメモリが解放されることを保証します。このようにして、プログラマーは動的に割り当てられたメモリを手動で管理する必要がなくなります。

最新の C++ プログラミング (since C++11) 、標準ライブラリにはスマート ポインターが含まれています。 C++11 3 種類のスマート ポインター std::unique_ptr があります 、std::shared_ptrstd::weak_ptr .これらのスマート ポインターは、<memory> の std 名前空間で定義されています。 ヘッダファイル。したがって、<memory> を含める必要があります これらのスマート ポインターを使用する前に、ヘッダー ファイルを削除してください。

これらのスマート ポインターを 1 つずつ見ていきますが、それらを使用する前に、スマート ポインターの動作を理解し、独自のスマート ポインターを実装しましょう。

スマート ポインターの実装:

スマート ポインターは、生のポインターをラップし、-> をオーバーロードする単なるクラスです。 と * オペレーター。これらのオーバーロードされた演算子を使用すると、生のポインターと同じ構文を提供できます。これは、スマート ポインター クラスのオブジェクトが通常のポインターのように見えることを意味します。

次の単純な SmartPointer を考えてみましょう クラス。 -> をオーバーロードしました と * 演算子と、クラス デストラクタに削除の呼び出しが含まれています。

class SmartPointer
{
public:
    // Constructor
    explicit SmartPointer(int* ptr) : m_ptr(ptr) {}

    // Destructor
    ~SmartPointer()
    {
        delete m_ptr;
    }

    // Overloading dereferencing operator
    int& operator* ()
    {
        return *m_ptr;
    }

    // Overloading arrow operator
    int* operator->()
    {
        return m_ptr;
    }

private:
    int* m_ptr;
};

SmartPointer クラスは、スタックに割り当てられたオブジェクトとして使用できます。 スマート ポインタはスタック上で宣言されているため、スコープ外になると自動的に破棄されます .また、コンパイラはデストラクタを自動的に呼び出します。スマート ポインター デストラクタには、割り当てられたメモリを解放する削除演算子が含まれています。

SmartPointer クラスを使用している次の C++ プログラムを考えてみましょう。動的メモリがこのクラスによって自動的に処理されることがわかります。メモリの割り当て解除について心配する必要はありません。

#include <iostream>
using namespace std;

class SmartPointer
{
public:
    // Constructor
    explicit SmartPointer(int* ptr) : m_ptr(ptr) {}

    // Destructor
    ~SmartPointer()
    {
        cout<<"Release the allocated memory\n";
        delete m_ptr;
    }

    // Overloading dereferencing operator
    int& operator* ()
    {
        return *m_ptr;
    }

    // Overloading arrow operator
    int* operator->()
    {
        return m_ptr;
    }

private:
    int* m_ptr;
};


int main()
{
    SmartPointer ptr(new int(27));

    //print the value
    cout<< *ptr <<endl;

    //Assign a value
    *ptr = 10;

    //print the value
    cout<< *ptr <<endl;

    return 0;
}

Output:

前述の SmartPointer クラスは整数に対してのみ機能します。ただし、C++ テンプレートを使用して汎用にすることができます。以下の例を考えてみてください。

#include <iostream>
using namespace std;

//Generic smart pointer class
template <class T>
class SmartPointer
{
public:
    // Constructor
    explicit SmartPointer(T* ptr) : m_ptr(ptr) {}

    // Destructor
    ~SmartPointer()
    {
        cout<<"Release the allocated memory\n";
        delete m_ptr;
    }

    // Overloading dereferencing operator
    T& operator* ()
    {
        return *m_ptr;
    }

    // Overloading arrow operator
    T* operator->()
    {
        return m_ptr;
    }

private:
    T* m_ptr;
};

class Display
{
public:
    void printMessage()
    {
        cout<<"Smart pointers for smart people\n\n\n";
    }
};


int main()
{
    //With integer
    SmartPointer<int> ptr(new int(27));

    //print the value
    cout<< *ptr <<endl;

    //Assign a value
    *ptr = 10;

    //print the value
    cout<< *ptr <<endl;


    //With custom class
    SmartPointer<Display> ptr1(new Display());
    ptr1->printMessage();

    return 0;
}

Output:

Remark: 上記のスマート ポインターの実装コードは、スマート ポインターの概念を理解するためにのみ作成されています。この実装は、多くの実際のケースには適していません。また、現実的なスマート ポインターの完全なインターフェイスではありません。

スマート ポインターの種類:

次のセクションでは、C++11 で使用できるさまざまな種類のスマート ポインターをまとめ、それらをいつ使用するかについて説明します。

unique_ptr:

これは、C++ 標準ライブラリの ヘッダーで定義されています。基本的に、一意のポインターは、別のオブジェクトを所有し、ポインターを介してその別のオブジェクトを管理するオブジェクトです。一意のポインターは、それが指すオブジェクトの排他的所有権を持ちます。

例で unique_ptr を理解しましょう。U とします。 2番目のオブジェクトへのポインタを格納する一意のポインタのオブジェクトです P .オブジェクト U P を破棄します U の場合 それ自体が破壊されます。このコンテキストでは、U P を所有していると言われています .

また、unique_ptr はそのポインターを他の unique_ptr と共有しないことに注意してください。これは移動のみ可能です。これは、メモリ リソースの所有権が別の unique_ptr に譲渡され、元の unique_ptr がそれを所有しなくなることを意味します。

次の例は、unique_ptr インスタンスを作成する方法と、所有権を別の一意のポインターに移動する方法を示しています。

#include <iostream>
#include <memory>
using namespace std;


class Test
{
public:
    void print()
    {
        cout << "Test::print()" << endl;
    }
};

int main()
{
    /*
    Create an unique pointer
    object that store the pointer to
    the Test object
    */
    unique_ptr<Test> ptr1(new Test);

    //Calling print function using the
    //unique pointer
    ptr1->print();

    //returns a pointer to the managed object
    cout << "ptr1.get() = "<< ptr1.get() << endl;

    /*
    transfers ptr1 ownership to ptr2 using the move.
    Now ptr1 don't have any ownership
    and ptr1 is now in a 'empty' state, equal to `nullptr`
    */
    unique_ptr<Test> ptr2 = move(ptr1);
    ptr2->print();

    //Prints return of pointer to the managed object
    cout << "ptr1.get() = "<< ptr1.get() << endl;
    cout << "ptr2.get() = "<< ptr2.get() << endl;

    return 0;
}

出力:

Remark: その用途には、動的に割り当てられたメモリの例外安全性、動的に割り当てられたメモリの所有権を関数に渡す、動的に割り当てられたメモリを関数から返すなどがあります。

shared_ptr:

shared_ptr は、メモリ内のオブジェクトの有効期間が複数の所有者によって管理されるシナリオ向けに設計されたスマート ポインターの一種です。 shared_ptr という意味です 共有所有権のセマンティクスを実装します。

unique_ptr と同様に、shared_ptr も C++ 標準ライブラリの ヘッダーで定義されています。共有所有権の概念に従っているため、shared_ptr を初期化した後、それをコピー、割り当て、または関数の引数で値によって渡すことができます。すべてのインスタンスは、割り当てられた同じオブジェクトを指しています。

shared_ptr は、参照カウント ポインターです。参照カウンターは、新しい shared_ptr が追加されるたびに増加し、shared_ptr が範囲外になるかリセットされるたびに減少します。参照カウントがゼロになると、指定されたオブジェクトは削除されます。これは、ポインターの最後に残った所有者がオブジェクトを破棄する責任があることを意味します。

Remark: ポインターを所有していない場合、shared_ptr は空であると言われます。

次の例は、shared_ptr インスタンスを作成する方法と、所有権を別の shared_ptr ポインターと共有する方法を示しています。

#include <iostream>
#include <memory>
using namespace std;

class Test
{
public:
    void print()
    {
        cout << "Test::print()" << endl;
    }
};

int main()
{
    /*
    Create an shared ptr
    object that store the pointer to
    the Test object
    */
    shared_ptr<Test> ptr1(new Test);

    //returns a pointer to the managed object
    cout << "ptr1.get() = "<< ptr1.get() << endl;
    //print the reference count
    cout << "ptr1.use_count() = " << ptr1.use_count() << endl;


    cout <<"\nCreate another shared pointer "
         "and Initialize with copy constructor.\n";
    /*
     Second shared_ptr object will also point to same pointer internally
     It will make the reference count to 2.
    */
    shared_ptr<Test> ptr2(ptr1);

    cout << "Prints return of pointer to the managed object\n";
    cout << "ptr1.get() = "<< ptr1.get() << endl;
    cout << "ptr2.get() = "<< ptr2.get() << endl;


    cout <<"\nprint the reference count after creating another shared object\n";
    cout << "ptr1.use_count() = " << ptr1.use_count() << endl;
    cout << "ptr2.use_count() = " << ptr2.use_count() << endl;

    // Relinquishes ownership of ptr1 on the object
    // and pointer becomes NULL
    cout <<"\nprint the reference count after reseting the first object\n";
    ptr1.reset();
    cout << "ptr1.get() = "<< ptr1.get() << endl;
    cout << "ptr2.use_count() = " << ptr2.use_count() << endl;
    cout << "ptr2.get() = "<< ptr2.get() << endl;

    return 0;
}

Output:

ptr1.get() = 0xf81700
ptr1.use_count() = 1

Create another shared pointer and Initialize with copy constructor.
Prints return of pointer to the managed object
ptr1.get() = 0xf81700
ptr2.get() = 0xf81700

print the reference count after creating another shared object
ptr1.use_count() = 2
ptr2.use_count() = 2

print the reference count after reseting the first object
ptr1.get() = 0
ptr2.use_count() = 1
ptr2.get() = 0xf81700

weak_ptr

weak_ptr shared_ptr によって既に管理されているオブジェクトへの弱い参照を格納するスマート ポインターです。 . weak_ptr はオブジェクトの所有権を取得しませんが、オブザーバーとして機能します (weak_ptrs は共有観測用です)。これは、オブジェクトを削除したり、その有効期間を延長したりするための参照カウントに参加しないことを意味します。私たちは主にweak_ptrを使用して、std::shared_ptrによって管理されるオブジェクトによって形成された参照循環を断ち切ります。

オブジェクトにアクセスするメンバー関数ロックを使用して、weak_ptr を shared_ptr に変換できます。これは、weak_ptr を使用して、それが初期化された shared_ptr の新しいコピーを取得しようとすることができることを意味します。メモリが既に削除されている場合、weak_ptr の bool 演算子は false を返します。

おすすめの記事:

  • C++ プログラミング コースとチュートリアル
  • C++ で一意のポインタを作成して使用する方法
  • 動的メモリ用の C++ の新しい演算子
  • malloc() と new.
  • C++ での参照の導入
  • C/C++ のポインタ
  • C++ 面接の質問と回答
  • 最高の C++ 書籍のリストです。必見です。
  • 動的メモリ割り当てに関するインタビューの質問

参考文献:
動的メモリ管理。