Version 17.28
スマートポインタのまとめ
「さて、今回は前回まで説明してきたスマートポインタをまとめます」
『なんかすっごく複雑だったんだけど……』
「まず、スマートポインタの目的は、 new した変数を〈使わなくなった〉
時に delete する、というものです」
『使ってるときに delete しちゃダメだし、 delete し忘れたら
メモリリークしちゃうから、ってことね』
「そういうこと。つまり、 Version 17.21 ( No.376 ) の以下の箇所でして
いる delete を、自動的に行いたいわけです」
int WINAPI WinMain
( HINSTANCE p_hInstance
, HINSTANCE p_hPrevInstance
, LPSTR p_pchCmdLine
, int p_iCmdShow
)
{
// 出力用に、 CPrinter クラスのポインタを受け取ります。
CPrinter *pcPrinter;
// まずデバッグ用を受け取ります。
pcPrinter = GetPrinterInstance( DEBUG_PRINTER );
// 出力します。
pcPrinter->Output( "あいうえお\n" );
// 解放します。
delete pcPrinter; ←ここと、
// 次にダイアログ用を受け取ります。
pcPrinter = GetPrinterInstance( DLG_PRINTER );
// 出力します。
pcPrinter->Output( "あいうえお\n" );
// 解放します。
delete pcPrinter; ←ここ。
return 0;
}
『スマートポインタを使用することで、これが可能になります』
int WINAPI WinMain
( HINSTANCE p_hInstance
, HINSTANCE p_hPrevInstance
, LPSTR p_pchCmdLine
, int p_iCmdShow
)
{
// 出力用に、 CSmartPointer クラスを受け取ります。
// まずデバッグ用を受け取ります。
CSmartPointer cSmartPointer = GetPrinterInstance( DEBUG_PRINTER );
// 出力します。
cSmartPointer.GetPointer()->Output( "あいうえお\n" );
// 次にダイアログ用を受け取ります。
cSmartPointer = GetPrinterInstance( DLG_PRINTER );
↑ここで以前持っていたポインタを delete します。
// 出力します。
cSmartPointer.GetPointer()->Output( "あいうえお\n" );
return 0;
↑ここで持っていたポインタを delete します。
}
『こうすることでメモリリークしなくなるわけね。でも……』
「でも?」
『難しすぎるよこれ! こんなの絶対作れないし、使うのもかなり無理!』
「うん、難しいと思うよ」
『へ?』
「これは、 C++ 言語の〈一番難しい部分〉です」
『一番難しいとこなの?』
「そう。 C++ 言語には、コンストラクタと演算子のオーバーロードの複雑
なルールがあって、これをパズルのように組み合わせることで
スマートポインタのような特殊なクラスを作ることができるんです」
『パズル……』
「そう、本当にパズル。元々のルール、 C++ 言語の複雑な言語仕様をまず
熟知しなきゃいけないし、それを使いこなせなきゃいけない。それはかなり
大変なこと。これができたら上級者って言ってもいいと思うよ」
『そんな難しい部分なんだ……』
「実際、スマートポインタを作るのはかなり大変です。もしミスがあったら
メモリリークが発生したりまだ使用中なのに delete してしまう可能性があ
るから」
『ミスってどんな?』
「たとえばコピーコンストラクタで参照カウンターを増やし忘れるとか」
// コピーコンストラクタ。
CSmartPointer( const CSmartPointer &p_rcSmartPointer )
: m_pcPrinter( p_rcSmartPointer.m_pcPrinter )
, m_piRefCounter( p_rcSmartPointer.m_piRefCounter )
{
// ← AddRef() を呼んでない。
}
「これを呼び忘れると、以下の箇所で問題になります」
return cSmartPointer;
↓
CSmartPointer cSmartPointer = cSmartPointer;
(左) (右)
「右の cSmartPointer はすぐになくなるけど、 AddRef() メンバ関数を呼
び出していないと参照カウンターが 0 になっちゃって」
『げ、その場で delete されちゃう!?』
「というわけ」
『……でもさ、そんなミスってする?』
「するんだよねこれが。たとえば〈コピーコンストラクタを作り忘れた〉と
か」
『……へ?』
「このプログラム、コピーコンストラクタがなくてもビルドできるでしょ」
『げ! なんで……あ! そだ、コピーコンストラクタって元々作られてる
コンストラクタだった!』
「そう、 Version 16.11 ( No.338 ) と Version 16.12 ( No.339 ) で説明
したように、コピーコンストラクタは作らなくても元々存在してるんです」
『作らなかった場合、メンバ変数をコピーする……つまり、さっきの
AddRef() メンバ関数を呼んでないコピーコンストラクタと同じ!』
「そういうこと。だから、スマートポインタにコピーコンストラクタを作り
忘れるとそれだけでバグになるわけです」
『うわぁ……』
「これを忘れないようにするためには、コピーコンストラクタっていうもの
が存在すること、それを自分で作れることを知らないといけないわけです」
『……でも、あのときも思ったけど、見えないところでそういう機能が動い
てる、っていうのを知らないといけないって大変だね』
「だから一番難しい部分、ってこと。こういう〈自動的に行われている部分〉
っていうのは、普通にプログラムを組んでいる時には〈気にしなくてもいい〉
ところなんです」
『だよね、勝手にしてくれて便利ってゆーか』
「だけど、スマートポインタみたいなものを作るためには、その奥まで知っ
ていないといけない、というわけなんです」
『うわぁ……』
「さすがにここまで勉強するのは相当大変だけど、 C++ を使ううえではい
つか必要になる知識だから」
『それまでこつこつ勉強ね……』
「さて、ちょっと話を戻して、さっきはコピーコンストラクタを作らないと
うまく動かない例を見てもらったけど、ちゃんと作ってても使い方次第で
正しく動作しない場合があるんです」
『げ! それってどんなとき?』
「具体的には、中で持っているはずのアドレスを〈直接〉扱ってしまうと
うまく動きません。たとえば」
// スマートポインタを受け取りました。
CSmartPointer cSmartPointer = GetPrinterInstance( DEBUG_PRINTER );
// このなかにあるアドレスを、取り出してポインタに代入します。
CPrinter *pcPrinter = cSmartPointer.GetPointer();
『そか、スマートポインタの中のアドレスって GetPointer() メンバ関数で
取得できるんだったね。でもこれがいけないの?』
「これをしてしまうと、ひとつのアドレスが cSmartPointer と pcPrinter
の両方に入ってることになります。こうなってしまうと、スマートポインタ
では管理しきれません」
『どして?』
「スマートポインタは〈もう参照先の変数を使わなくなったとき〉、つまり
〈アドレスがどのポインタにも入ってないとき〉に delete で削除します」
『うん、それがスマートポインタの目的だもんね』
「でも、普通のポインタにアドレスが入れられていても、スマートポインタ
はそのことは分からないから……」
『げ! そか、スマートポインタだけ使ってたら〈全部なくなった〉ってい
うことが参照カウンターでわかるけど、普通のポインタも混ざってると……』
「だから、たとえばこういうことが起きてしまいます」
// ポインタだけ先に作ります。
CPrinter *pcPrinter;
// ブロック(実際には if とか for とか)。
{
// スマートポインタを受け取りました。
CSmartPointer cSmartPointer
= GetPrinterInstance( DEBUG_PRINTER );
// このなかにあるアドレスを取り出してポインタに代入します。
pcPrinter = cSmartPointer.GetPointer();
// このブロックから抜けると、スマートポインタは
// 中のポインタを delete してしまいます。
}
pcPrinter->Output( "あいうえお\n" );
// ↑ダメ! もう delete されたアドレスを使ってます!
『えと、ブロックの中でさっきと同じことしてるんだよね。スマートポインタ
作って、その中のアドレスを外のポインタに渡して……げ! そか、ブロック
から出るとスマートポインタがなくなっちゃうから……』
「デストラクタが呼び出されて中にあるアドレスが delete されてしまうん
です」
『そうすると、 pcPrinter で持ってるアドレスも同じだから、もう使えな
くて……つまり、普通のポインタを持っていると、こういうふうに使えなく
なったアドレスを持たされるハメになる、と』
「そういうこと。逆に、普通のポインタが、スマートポインタの悪さをする
可能性もあるんです」
『どゆこと?』
「たとえば、勝手に delete しちゃうとか」
// スマートポインタを受け取りました。
CSmartPointer cSmartPointer = GetPrinterInstance( DEBUG_PRINTER );
// このなかにあるアドレスを、取り出してポインタに代入します。
CPrinter *pcPrinter = cSmartPointer.GetPointer();
// (ここで何か処理)
// 使い終わったから、ポインタを delete しましょう。
delete pcPrinter;
// でもこのあと cSmartPointer 変数でも delete するから
// 二重に delete されてしまいます……。
『ああーっ! そか、確かにポインタがあったら delete したくなっちゃう
ね……』
「実際のプログラムではもっと複雑になるから、こういうことはより起こり
やすくなります。で、こういうミスをしないためにも、仕組みを知ってる
必要があるわけです、が」
『それは実はかなり複雑、 C++ じゃ一番難しいレベル』
「でも、ポリモーフィズムでは new や delete は必要不可欠」
『 Version 17.21 ( No.367 ) でそう言われました……』
「だから、ずーっと勉強していったその先に、こういう分野があるっていう
ことを覚えておいて」
『ちょっと忘れたいかも……』
/*
Preview Next Story!
*/
『なんか今回はへこんだかも……』
「まぁこの辺は本当に難しい部分だからね……」
『ここが一番難しい? 難しいとこってここくらい?』
「あとは const メンバ関数くらいかな」
『げ、イヤなこと思い出させられた』
「というわけで次回」
< Version 17.29 継承とコンストラクタ >
『につづく!』
「ポリモーフィズムも実はもっと複雑だし、他にもテンプレートとか……」
『いやっもう聞きたくない!!』