スマートポインタ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を置き換えるスマートポインタが導入され、そちらの使用が推奨されています。