スマートポインタ1
C++流の「新しいポインタ」
C++でのメモリ確保には主にnew演算子を使用しますが、この確保領域にdelete演算子で解放処理を書くのはプログラマの責任です。
これを忘れるとメモリリーク(解放されないメモリ領域が溜まる現象)が発生します。
これは仕方のないことなので、deleteを忘れないように気を付けるのですが、コードが複雑になると解放を忘れてしまうことが往々にしてあります。
この「解放し忘れ」を防ぐための新しい手法として、スマートポインタというものが導入されています。
このページで説明しているstd::auto_ptr
クラスはC++11で非推奨となり、C++17以降では削除されています。
最新のコンパイラではこのページのコードはコンパイルできない可能性があります。
しかしスマートポインタの概念の理解のために、このページは目を通しておくことをおすすめします。
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(); //"2"を出力
}
int main()
{
func();
//この時点でtcBのインスタンスはメモリ上から消えている
std::cin.get();
}
コンストラクタ: 1 コンストラクタ: 2 2 デストラクタ: 2
スマートポインタを使用するには#include <memory>
が必要です。
関数func
内では二種類の方法でTestClass
のインスタンスを生成しています。
ポインタ変数tcA
は従来通りnew
による方法でメモリを確保していますが、コード中でdelete
を実行していないのでデストラクタが呼ばれません。
(関数終了後は解放する手段がないのでメモリリークが発生する)
もう一方のインスタンスtcB
もdelete
によってインスタンスを破棄していませんが、TestClassのデストラクタが呼ばれています。
tcB
は正確にはTestClass
のポインタ変数ではなく、std::auto_ptr
というスマートポインタのインスタンスです。
auto_ptr
は内部的にはクラスで実装されていて、コンストラクタにnewで確保したポインタを渡すとそのポインタはauto_ptr
によって「管理」された状態となります。
auto_ptrのデストラクタでは、管理しているポインタにdeleteが実行されます。
tcB
はローカル変数なので、関数func
の終了時にデストラクタが呼ばれ、管理しているTestClass
がdeleteされインスタンスが破棄されるわけです。
auto_ptr
のインスタンス自体はポインタではありませんが、通常のポインタと同じ感覚で扱えます。
つまり間接演算子(*
)で値にアクセスしたり、アロー演算子(->
)でメンバにアクセスしたりすることができます。
ただし、特定のオブジェクトを管理するため++
や--
などのポインタ演算はできません。
このあたりはポインタよりも参照に近いです。
deleteでメモリを解放しなくても、プログラム終了時にはすべてのメモリは解放されます。
auto_ptrで
管理できるのはnew
演算子で確保したメモリ領域だけです。
C言語のmalloc
などで確保したメモリ領域は管理できません。
その他のスマートポインタではmalloc
等のポインタも管理可能です。
生ポインタの取得
スマートポインタが管理している「生のポインタ」はget
関数で取得することができます。
std::auto_ptr<TestClass> ptr(new int(1));
int *p = ptr.get();
メモリを管理していない場合はNULL(0)を返します。
この方法で取得した生ポインタに対してdelete
は実行しないでください。
auto_ptr
のインスタンス破棄時にもdelete
が実行されるので、二重解放になってしまいます。
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));
どうしてもポインタ変数を作らねばならない場合、スマートポインタに管理を任せたらすぐにnullptr
を代入しておくと安全です。
int *p = new int(10);
std::auto_ptr<int> ptr(p);
p = nullptr; //pを使えなくする
delete p; //nullptrに対する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); //これでも同じこと
所有権の放棄
メモリの所有権は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
のインスタンスに代入すると、今まで所有していたポインタの所有権を失います。
(管理するポインタは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();
}
このコードでは、コピーコンストラクタを利用してインスタンスのコピーを作ったつもりが、tcA
のメンバ変数が管理していたポインタの所有権がtcB
に移動してしまいます。
そのままtcA
を使用するとNULLポインターへのアクセスが発生し、実行時エラーとなります。
このような動作が意図したものであることはほぼないでしょう。
auto_ptr
はほかにも、インスタンス破棄時のメモリ解放処理がdelete
で固定という問題があります。
これはつまり、new
で確保したメモリ以外は管理できないということです。
また、配列を扱えないということでもあります。
(配列はdelete[]
でメモリ破棄する必要があるため)
これらの仕様が微妙に使いづらいため、C++11というバージョンからはauto_ptr
を置き換えるスマートポインタが導入され、そちらの使用が推奨されています。