スマートポインタ1

C++流の「新しいポインタ」

C++でnew演算子によりメモリを確保した場合、delete演算子で解放処理を書くのはプログラマの責任です。
これを忘れるとメモリリーク(解放されないメモリ領域が溜まる現象)が発生します。
これは仕方のないことなので、deleteを忘れないように気を付けるのですが、忘れてしまうことが往々にしてあります。

この「解放し忘れによるメモリリーク」をできるだけ防ぐための新しい手法として、スマートポインタというものが導入されています。

auto_ptr

まずは簡単な例を見てください。


#include <iostream>
#include <memory>

class TestClass
{
    int num;
public:
    TestClass(int n) : num(n)
    {
        std::cout << "コンストラクタ: " << num << std::endl;
    }
    ~TestClass()
    {
        std::cout << "デストラクタ: " << num << std::endl;
    }

    void print() { std::cout << num << std::endl; }
};

void func()
{
    //通常の方法
    TestClass* tcA = new TestClass(1);

    //auto_ptrによるメモリ確保
    std::auto_ptr<TestClass> tcB(new TestClass(2));

    //通常のポインタと同じように扱える
    tcB->print();
}

int main()
{
    func();
    //この時点でtcBのインスタンスはメモリ上から消えている

    std::cin.get();
}
コンストラクタ: 1
コンストラクタ: 2
2
デストラクタ: 2

スマートポインタを使用するには#include <memory>が必要です。

関数func内で二種類の方法でTestClassのインスタンスを生成しています。
インスタンスtcAは通常通りnewによる方法でメモリを確保していますが、コード中でdeleteしていないので、デストラクタが呼ばれません。

もう一方のtcBもdeleteによってインスタンスを破棄していませんが、TestClassのデストラクタが呼ばれています。

auto_ptrというのがスマートポインタで、これはクラスの機能で実装されています。
auto_ptrのコンストラクタにポインタを渡すと、auto_ptrによって「管理」された状態となります。

auto_ptrのインスタンス自体はポインタではありませんが、通常のポインタと同じ感覚で扱えます。
つまり間接演算子(*)で値にアクセスしたり、アロー演算子(->)でメンバにアクセスしたりすることができます。

ただし、ポインタ変数はそのままでアドレスを表すのに対して、auto_ptrのインスタンスはあくまでもクラスのインスタンスであり、それ自体はアドレスを表しません。
アドレスを取得するにはget関数(後述)で管理している「生ポインタ」を取り出します。
また、++や--などのポインタ演算はできません。

auto_ptrのインスタンスであるtcBは関数funcのローカル変数ですから、その寿命は関数funcの終了までです。
クラスのインスタンスですから、寿命が尽きるときにデストラクタが呼ばれます。
auto_ptrのデストラクタは、自身が管理しているメモリ領域に対してdeleteを実行します。
サンプルコードではTestClassのインスタンスに対してdeleteすることになりますから、TestClassのデストラクタが呼び出されメモリが解放されることになります。

ちなみにtcAの寿命も関数funcの終了までですが、tcAは通常のポインタ変数ですから、deleteが自動で呼び出されることはありません。
どこかでdeleteを実行しない限り、メモリは確保されたままとなります。
サンプルコードでは、メモリを確保したままポインタ変数tcAの寿命が尽きています。
こうなると確保したメモリ領域にアクセスする手段がなくなるため、メモリリークが発生してしまいます。
(ポインタ変数の寿命が尽きる前にdeleteするか、関数の戻り値にセットして外部からdeleteできる手段を確保する必要がある)

deleteでメモリを開放しなくても、プログラム終了時にはすべてのメモリは解放されます。

auto_ptrで管理できるのはnew演算子で確保したメモリ領域だけです。
C言語のmallocなどで確保したメモリ領域は管理できません。
その他のスマートポインタではmalloc等のポインタも管理可能です。

生ポインタの確認

スマートポインタが管理している「生のポインタ」はget関数で取得することができます。


std::auto_ptr<TestClass> ptr(new int(1));

int *p = ptr.get();

この方法で取得した生ポインタに対してdeleteは実行しないでください。
auto_ptrのインスタンス破棄時にもdeleteが実行されますので、二重解放になってしまいます。

メモリを管理していない場合はNULLを返します。
get関数は実際にメモリを管理しているかどうかのチェックや、C言語の関数や他のライブラリ等でどうしても生のポインタが必要なときに使用する、といった程度に留めましょう。

所有権

スマートポインタには所有権という概念があります。
所有権は、「確保したメモリ領域にアクセスする権利」と「確保したメモリを開放する義務」を誰が持っているかを示すものです。

以下は、newにより確保したメモリへのアクセスはptrを介して行い、ptrにメモリの解放を任せる、という意味になります。


//newでメモリを確保し、スマートポインタに所有権を渡す
std::auto_ptr<TestClass> ptr(new TestClass());

以下のコードは意図した動作とはなりません。
ポインタをスマートポインタに渡した場合、それ以降はそのポインタ変数は使用すべきではありません。


int *p = new int(10);
std::auto_ptr<int> ptr(p);
delete p;

//意図しない値を出力
std::cout << *ptr << std::endl;

auto_ptrのコンストラクタ内でメモリを確保する処理を書けば、余計なポインタ変数を作らずに済むので危険なアクセスをしてしまう可能性はなくなります。


//良くない例
int *p = new int(10);
std::auto_ptr<int> ptr(p);

//良い例
std::auto_ptr<int> ptr(new int(10));

どうしてもポインタ変数を作らねばならない場合、スマートポインタに管理を任せたらすぐにNULLを代入しておくと安全です。


int *p = new int(10);
std::auto_ptr<int> ptr(p);

p = NULL; //pを使えなくする

delete p; //NULLに対するdeleteは安全

//10を表示
std::cout << *ptr << std::endl;

所有権の取得

auto_ptrは、コンストラクタにポインタを渡すことで所有権を得ますが、reset関数で所有権を得ることもできます。


std::auto_ptr<int> ptr(new int(1));
ptr.reset(new int(2));

一行目ではauto_ptrのコンストラクタで所有権を得ています。
二行目ではreset関数で別のポインタを渡し、新たなメモリの所有権を得ています。

この時、すでに所有していたメモリに対しては自動的にdeleteが行われ、メモリを解放してから新たなメモリの管理を開始します。
よって、deleteのし忘れという心配はありません。

また、reset関数を引数なしで呼び出す、またはNULLやnullptrを引数に指定して呼び出すことで、管理しているメモリ解放することができます。


std::auto_ptr<int> ptr(new int(1));

ptr.reset(); //メモリの開放のみを行う
ptr.reset(nullptr); //これでも同じこと

nullptrはC++11から追加されたキーワードで、「何も指していないポインタ」を意味します。
NULLとほぼ同じものですが、微妙に違いがあります。
詳しく説明はしませんが、「何も指していないポインタ」の意味で使いたい場合はnullptrの使用が推奨されます。

所有権の放棄

メモリの所有権はrelease関数で放棄することができます。


std::auto_ptr<int> ptr(new int(1));

//release関数は管理しているポインタを返す
int *p = ptr.release();

//手動でdeleteが必要
delete p;

注意点としては、release関数は所有権を放棄するだけで、管理しているポインタに対してdeleteは行わないという事です。
release関数の戻り値は現在管理しているポインタなので、後で自分でdeleteする必要があります。

所有権の移動

auto_ptrは便利な機能ですが、最近のコンパイラ(C++11以降)では非推奨となっています。
それは所有権が意図しないところで移動してしまうことがあるためです。


std::auto_ptr<int> ptr1(new int(1));
std::auto_ptr<int> ptr2;

ptr2 = ptr1; //コピー...のつもり

std::cout << *ptr1 << std::endl; //エラー

代入演算子=は、通常であれば「コピー」の動作を行います。
しかしauto_ptrの場合は「コピー」ではなく「所有権の移動」という動作になります。
そのため、auto_ptrのインスタンスを別のauto_ptrのインスタンスに代入すると、今まで所有していたポインタの所有権を失います。
(ptr1はNULLになってしまいます)

上のような単純なコードではミスすることはないかもしれませんが、「コピー処理がコピーにならない」というのはミスを犯す可能性を上げてしまいます。


#include <iostream>
#include <memory>

class TestClass
{
    std::auto_ptr<int> ptr;
public:
    TestClass(int n) : ptr(new int(n))
    {}

    void print() { std::cout << *ptr << std::endl; }
};

int main()
{
    TestClass tcA(1);
    TestClass tcB(tcA); //この時点で所有権が移動している!

    //tcA.print(); エラー
    tcB.print();
}

このコードでは、コピーコンストラクタを利用してインスタンスのコピーを作ったつもりが、auto_ptrを利用したメンバ変数の所有権が移動してしまいます。

auto_ptrはほかにも、終了処理がdeleteで固定という問題があります。
これはつまり、newで確保したメモリ以外は管理できないということです。
また、配列を扱えないということでもあります。
(配列は「delete[]」でメモリ破棄する必要がある)

そのためC++11というバージョンからはauto_ptrを置き換えるスマートポインタが導入され、そちらの使用が推奨されています。