C++の勉強(2)

C++の勉強

Effective C++を読んで参考になったことを書いておく. とりあえず第三章まで読んだ.

コピーコンストラクタについて.

コピーコンストラクタの必要性が全くわからなかった.それはいま書いているコード特有の問題だったので,すっきりした.

サンプルコード

次の機能を持つNumericクラスを考え,main関数での動作も同時に定める. Numericクラスの仕様 1. int型を保持する動的配列numをメンバ変数とする.インスタンス化の時点でコンストラクタが呼ばれ,メンバ変数lengthが初期化される.さらに,すべての要素が0で初期化される.
2. メンバ関数Set(N)は整数Nをnumのすべての要素に代入する.
3. メンバ関数Print()numの0番目の要素を出力する.
4. メンバ関数Compare(N)は,そのオブジェクトのnumの0番目の要素と,別のオブジェクトNのそれを比較して,同じ大きさか違う大きささかを出力する.

このクラスの実装とmain関数で簡単な動作確認を行うためのコードが次の通り. コピーコンストラクタの意義1

サンプルコードの動作と予想される結果

Numericクラスは一見問題なく動くように見え,その結果は以下の通りになると予想される.

num[0] = 100
num[0] = 50
ちがう大きさ

しかしながら実際に動かしてみると,上記の結果が出力されたあと,

malloc: *** error for object (ポインタが指しているアドレス): pointer being freed was not allocated

と出力されて正常に終了しない.この理由がコピーコンストラクタを定義していないことにある.

なぜうまくいかないか

オブジェクト(ここではインスタンス化されたNumericクラスn1n2)を関数に渡すとき,すべてのメンバ変数が値渡しされ,その関数のスコープ内でローカルなNumericクラスのオブジェクトnが生成される(これが暗黙に定義されるコピーコンストラクタの定義).そして,ここに落とし穴がある.メンバ変数numint型のポインタだから,nのメンバ変数numn2のメンバ変数numで初期化される.すると,その関数が終了し,スコープを外れる時点でオブジェクトnは破棄されるので,nのデストラクタが呼ばれる.この時,n とn2numは同じアドレスを指していることから,nの破棄を行うことは,n2numdeleteすることと全く同じ意味になる.したがって,その関数のスコープを外れたあとでn2numを用いて行う動作はすべて不定になる.
上で述べたエラーはn2numdeleten1->Compare(n2)で行われているのにもかかわらず,main関数のスコープをはずれて,n2が破棄される際にデストラクタが呼ばれたことが原因で生じているのである.

どうしたらよいか

Numericクラスにコピーコンストラクタを定義する.具体的には次の通り.

コピーコンストラクタの意義

まずかった原因は,クラスオブジェクトを引数にもつ関数を呼んだ際に,ポインタ変数のコピーのされ方が一致してしまうことなので,単にコピーコンストラクタが呼ばれた際のポインタ配列のコピーのされ方を新たに決めてやればいいという話.ここでは,配列の要素ごとに代入している.この結果,ここで生じたメモリの二重解放エラーはなくなる.

まとめ

ポインタ変数をメンバー変数に持つクラスがあるなら,コピーコンストラクタを定義しておかないと思わぬエラーの原因になる.