概要
C++においてオブジェクトの動的生成を行うと、オブジェクトを利用しなくなった時点で明示的にdeleteしてあげる必要があると思います。deleteをし忘れることはメモリリークを引き起こしてしまうので、メモリの動的確保には注意が必要です。それをサポートしてくれるスマートポインタというものがあるので実際に使って動作を確認してみたいと思います。
1. std::auto_ptr
ある一つのオブジェクトを指し示すポインタの管理をしてくれます。ポインタの寿命(スコープ)が切れたタイミングでポインタの指し示すメモリの解放を行ってくれます。
# include <iostream>
class Object {
public:
Object(const int &uid = 0) {
_uid = uid;
std::cout << "Object Constructor" << std::endl;
}
~Object() {
std::cout << "Object Destructor" << std::endl;
}
public:
void printId() const {
std::cout << "uid: " << _uid << std::endl;
};
public:
int _uid;
};
int main(int argc, const char *argv[])
{
// auto_ptr
Object *obj = new Object(1);
std::auto_ptr<Object> autoPtr(obj);
autoPtr.get()->printId();
// delete obj は必要なし
}
Object Constructor
uid: 1
Object Destructor
deleteを行わなくても、動的に生成されたオブジェクトのデストラクタが呼ばれていることが確認できると思います。auto_ptrを使う注意点があって、同じオブジェクトを指し示すauto_ptrは宣言してはいけません。これは、同じオブジェクトのメモリを複数回解放しようとしてしまう危険性があるからです。
int main(int argc, const char *argv[])
{
// auto_ptr
Object *obj = new Object(1);
std::auto_ptr<Object> autoPtr_1(obj);
std::auto_ptr<Object> autoPtr_2(obj); // 同じオブジェクトを指し示す
}
Object Constructor
Object Destructor
Object Destructor
malloc: *** error for object 0x1001000e0: pointer being freed was not allocated
実際にやってみたところ予想通りエラーをはいてしまいました。また、STLが提供するコンテナクラスにも格納できなかったり、配列を確保することも出来ないみたいなので、少々不便です。
2. std::unique_ptr
基本的な動作はauto_ptrと同じです。しかし、auto_ptrの欠点をサポートしたのがunique_ptrになります。違う点が、
・代入演算子の使用禁止
・コンテナクラスに格納が可能、配列の確保が可能
などの点で違っており、auto_ptrより使いやすくなっているようです。ともかく普通に動かしてみたいと思います。
nt main(int argc, const char *argv[])
{
Object *obj = new Object(2);
std::unique_ptr<Object> uniqPtr(obj);
uniqPtr.get()->printId();
// delete obj 必要なし
}
Object Constructor
uid: 2
Object Destructor
auto_ptrを同じ挙動ですね。
int main(int argc, const char *argv[])
{
Object *obj = new Object(2);
std::unique_ptr<Object> uniqPtr(obj);
std::unique_ptr<Object> uniqPtr2 = uniqPtr; // コンパイルエラー。auto_ptrの場合はこれができてしまう。
}
次は、配列の確保も試してみましょう。
int main(int argc, const char *argv[])
{
std::unique_ptr<Object[]> uniqPtr(new Object[3]);
}
Object Constructor
Object Constructor
Object Constructor
Object Destructor
Object Destructor
Object Destructor
無事に生成と解放がうまくいっているようです。
auto_ptrはC++11からは非推奨らしいので代わりにunique_ptrを使っていきましょう。
3. std::shared_ptr
こちらは参照カウンタ型のスマートポインタになります。これを使うと同じオブジェクト指し示すスマートポインタを複数宣言することが可能です。ちなみにauto_ptrを使ってと代入演算子を利用して同じオブジェクトを指すスマートポインタを作成すると、
int main(int argc, const char *argv[])
{
std::auto_ptr<Object> autoPtr(new Object(3));
std::auto_ptr<Object> autoPtr2 = autoPtr; // 代入
std::cout << "autoPtr Address: " << autoPtr.get() << std::endl;
std::cout << "autoPtr2 Address: " << autoPtr2.get() << std::endl;
}
Object Constructor
autoPtr Address: 0x0
autoPtr2 Address: 0x1001000e0
Object Destructor
代入先のオブジェクトのアドレスは表示されていますが、代入元のものがnullptr(0x0)になってしまっています。代入際に右辺の値が変化するのはとても奇妙ですね。やはりauto_ptrを使うのはやや危険なようです。
そこで、shared_ptrを使って同じことをしてみましょう。
int main(int argc, const char *argv[])
{
std::shared_ptr<Object> sharedPtr = std::make_shared<Object>(3);
std::shared_ptr<Object> sharedPtr2 = sharedPtr;
std::cout << "sharedPtr Address: " << sharedPtr.get() << std::endl;
std::cout << "sharedPtr2 Address: " << sharedPtr2.get() << std::endl;
}
Object Constructor
sharedPtr Address: 0x100103a98
sharedPtr2 Address: 0x100103a98
Object Destructor
結果を見てみると、同じオブジェクトのアドレスも格納できていますし、デストラクタも一度だけしっかり呼ばれているようです。
4. std::weak_ptr
shared_ptrによって指し示されているオブジェクトへの”弱参照”を保持することができます。弱参照という言葉を初めて聞いたのですが、いろいろ説明を見ているとshare_ptrの扱っているオブジェクトへの参照カウントには影響せずに同じオブジェクトを指し示すポインタが扱えると言ったイメージです。
実際にサンプルプログラムを書いてみたいと思います。
int main(int argc, const char *argv[])
{
std::weak_ptr<Object> weakPtr;
{
// あるオブジェクトへの参照カウント型スマートポインタ
// 参照カウント 1
std::shared_ptr<Object> sharedPtr1 = std::make_shared<Object>(4);
{
// 同じオブジェクトを指し示すスマートポインタ 参照カウント 2
std::shared_ptr<Object> sharedPtr2 = sharedPtr1;
// 同じオブジェクトを弱参照するスマートポインタ 参照カウント 2 (参照カウントは増えない)
weakPtr = sharedPtr1;
// 参照カウントチェック
std::cout << weakPtr.use_count() << std::endl;
}
// sharedPtr2のスコープが切れた 参照カウント 1 //
// 参照カウントチェック
std::cout << weakPtr.use_count() << std::endl;
}
// sharedPtr1のスコープが切れた 参照カウント 0
// 参照されているオブジェクトが解放される
// 参照カウントチェック
std::cout << weakPtr.use_count() << std::endl;
// weakPtrを利用して参照先の情報を再びチェック
if (auto spt = weakPtr.lock()) {
spt->printId(); // 参照先が解放されているのでここは呼ばれることはない
}
}
Object Constructor
2
1
Object Destructor
0
少しわかりづらいプログラムなってしまいましたが、リソースが解放された後に参照カウントをチェックしてみるとしっかりと0になっているのが確認できると思います。
結論
スマートポインタをうまく使いこなすことが出来ればメモリ管理の苦労が少し和らぐことがわかりました。しかし、利用する際はスマートポインタの挙動自体もしっかりと把握しておかなければなりませんね。まだC++は使い始めたばかりで理解不足なところもあるので、サンプルプログラム中にミスがありましたら申し訳ございません。
参考文献
C++11 unique_ptrのおはなし
C++11 スマートポインタの話
shared_ptr & weak_ptr