技術的雑談-ポインタ配列、vectorの落とし穴
環境
- 多分C++全般
- 配列を使う場合はCもかな。
現象
- ポインタを配列やvectorで管理している時にSIGSEGV(不正アクセス)発生
- vectorの末尾方面に入っているポインタ変数をdeleteした時に、プログラム終了時に不正アクセス。
- vector実体をコピーしたのに中身にアクセスすると不正アクセス。
原因
- ポインタの二重開放
- vectorのコピーの振る舞いの誤理解
解説
同じクラスの複数のインスタンスをvectorや配列に入れて管理する事はよくある話で、SIGSEGVで墜ちるのもよくある話なので、その原因のヒントをば。
ポインタ配列は主にCで使う話なので今回はvectorで。
往々にしてこんなコードを書いてしまったりします。
std::vector<Hoge*> vecHoge; …… Hoge* pPointer = new Hoge(); // Hogeの新しいインスタンスの作製 vecHoge.push_back(pPointer); // vectorの最後に今作ったHogeインスタンスを追加 delete pPointer; // pPointerの開放 …… // vecHogeの後始末 for (std::vector<Hoge*>::iterator itr = vecHoge.begin; itr != vecHoge.end; ++itr) { delete (*itr); ←何回目かのここでSIGSEGV発生することがある!(マチガイ1) } // vecHogeがなくなったことの確認 assert(vecHoge.size() == 0); ← 必ずassertがfailする(マチガイ2)
なんでか?
結論から言うとpPointerをdeleteしてしまったのが原因です。
プログラマーの性として「newしたらdelete」と脊髄反射してしまうのでやってしまいがちですが…。
ここでは2つのマチガイを犯しています。
【マチガイ:1】vecHogeに管理をゆだねたObjectをdeleteしている
vectorにpush_backした時点で「vectorがそのObject保存しているからpPointerは不要になっちゃったよね。だからdelete」とやってしまいがちですが、pPointerが持っているのはその行の後半で行われた「new Hoge()」によって作られたHogeオブジェクトへのアドレスです。仮にそれを「0x0123」だったとします。
で、push_back()するとvecHogeの末尾には「0x0123」が入ります。
vecHoge[n]を使う人(とコンパイラ)はそれが「Hoge*の配列だ」と知っているからそのポイント先をHogeオブジェクトとして使えますが、vector自体は単に「0x0123」という値を保持しているに過ぎません。
なので、その値が指す先がdeleteされようが何されようが知ったこっちゃありません。
【マチガイ:2】vecHogeの要素を無効にしたのにvecHoge自体の大きさを変えていない
マチガイ1で述べたようにvectorは単に値を保持しているだけなので、それがポインタなのか何なのかは知りません。
なのでfor文で先頭から取り出して「delete」してもvector自体の構成要素がdeleteされるわけではありません。
結果的に、上記のfor文が終わった時にはvecHogeは「無効ポインタの配列」になっただけで、「ポインタの配列」としての大きさは変わりません。
当然、.size()の結果は0よりも大きい値が入っています。
正確にはfor文を終わった後に「.clear()」してvecHogeの大きさを0にしてしまうか、for文を使わずに、
while(vecHoge.size() > 0) { Hoge* temp = vecHoge.erase(0); delete temp; }
などとやりながらvectorの内容を一つもぎ取って減らして、そのポインタを逐次削除するなどの処理が適当です。
尚、vectorにポインタを入れているときはまだマシなのですが、vectorにポインタではなくオブジェクトの実体を入れている場合はもっとわけわかめになるだけでなく、メモリーがいっぱい消費されます。CPUも食います。
例えば、
void methodX() { vector<Hoge> vecHoge2; ← Hogeの実体のvectorを宣言 for ( int i = 0; i < 3; ++i) ← 3回ループ { Hoge tempHoge; ← (1) 実体の作成 vecHoge2.push_back(tempHoge); ← (2) 実体のpush_back() } ← スコープの終了(A) // 何かの処理 for ( int j = 0; j < 3; ++j) { Hoge aaa = vecHoge2[j]; ← (3) vecHogeの中身を取り出し // aaaを使って何かの処理 aaa.changeHおげValue(); } ← スコープの終了(B) } ← スコープの終了(C)
って事をやると、
(1) Hogeオブジェクトが作成されます。
(2) vecHoge2にtempHogeがコピーして渡されます。
(3) vecHoge2からaへオブジェクトがコピーして渡されます。
3回ループしているので実に全部で6回もHogeオブジェクト実体のコピーが行われます。
仮にHogeオブジェクトが巨大なデータを持つオブジェクトだったら…目も当てられません。
さらに、実体に対してはスコープの終わりでデストラクタが呼ばれますから、tempHogeについては(A)で、aaaについては(B)で、そして忘れてはいけないvecHoge2の中の3個のHogeオブジェクトについては(C)でデストラクタが実行されます。
3個のオブジェクトを作っているからデストラクタは3回でしょ?ではありません。
さらにさらに、(3)の時点ではvecHoge2からHogeオブジェクトがコピーによって取り出されていることに注意して下さい。
よってその直後でaaaを通して何か変更をしてもvecHoge2の中のオブジェクトには何の変更も行われません。
これがポインタのvectorだったら取り出した後でも同じオブジェクトのメモリ領域をポイントしているのでvecHoge2の中のオブジェクトにも同時に変更がされるんですけどね…。米1個の違いだしコンパイラでエラーが出るわけでもないので、(B)の後でvecHoge2の中身に行おうとした操作が全く行われていないことがわかってバグとなる事でしょう。
トドメはHogeオブジェクトが中に他のオブジェクトのポインタ変数を持っていて、それがHogeインスタンス間で共用されていて、Hogeオブジェクトのデストラクトによってdeleteされるような構造になっている時です。
Hoge自体のインスタンスのコピーが行われるときに何気なくポインタ値のコピーを行ってしまうと、コピーされたHogeオブジェクト実体がデストラクトされるときに内部のポインタの二重開放が行われます。
ぱっと見には「Hogeの開放で問題がおきている」ように見えるので、ますますデバッグ者はパニックになります。
(Hogeが種別IDのようなものを持っていて、IDにあわせたカタログオブジェクトのポインタを持っていたりすると……最悪ですね。)
これを解決するには以下の点に注意するといいでしょう。
vectorにはオブジェクトの実体を入れない!
複数のインスタンスから共有されるオブジェクトにはスマートポインタなどを利用する
vectorの中には無効なポインタを残さない erase()してからdeleteする
いや、それだけでこの種の問題全てが解決するわけでは決して無いのですが、普段からそういう習慣をつけておくだけで後々厄介なバグを抱え込まなくて済みます。
また、本文では取り扱いませんでしたが、ポインタを入れたvectorをコピーする場合、コピー先のvectorにはポインタ値が値コピーされます。
ポイントしているオブジェクト(メモリ)はコピーされません。
当たり前の事ですが結構ハマりやすいので注意しましょう。
(自戒も含めて…。)
履歴
2009/03/14 -- 初版
技術的雑談へ戻る
突っ込み
- vecHoge.size() > 0が!vecHoge.empty()ではない理由は? - ひでと (2014年03月04日 02時27分46秒)
- > vectorにポインタではなくオブジェクトの実体を入れている場合はもっとわけわかめになるだけでなく、メモリーがいっぱい消費されます。 - ケイ (2015年12月04日 11時50分02秒)
- 配列もコピーすれば元の配列は変化しないコピーしてるんだからコピーコンストラクタが呼ばれるのは当然 - 名無しさん (2016年04月19日 18時03分34秒)
- 昔C(C89)で仕事してました。今になってc++を趣味で遊んでます。vecotにstructの実体をpushbackしてハマってましたが、本記事で解決しました。ありがとうございました。 - oldman (2018年03月08日 00時20分13秒)