トップ 一覧 検索 ヘルプ RSS ログイン

技術的雑談-ポインタ配列、vectorの落とし穴の変更点

  • 追加された行はこのように表示されます。
  • 削除された行はこのように表示されます。
!!!技術的雑談-ポインタ配列、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秒)
{{comment}}

[[技術的雑談]]へ戻る

{{trackback}}

[[技術的雑談]]へ戻る