2006-07-05
真夜中の3時過ぎですが、どうも寝付けないのでこんな時間に更新しています。寝ぼけて間違ったことを書く可能性が高いので、気付いた方は遠慮なくご指摘ください。
今回はコピーコンストラクタと代入演算子という、どちらもオブジェクトのコピーに関する操作についてです。
コピーコンストラクタは、オブジェクトのコピーを行うためのもので、特に何も定義しなくてもデフォルトのコピーコンストラクタがコンパイラによって用意されます。デフォルトのコピーコンストラクタは、オブジェクトの各メンバを単純にコピーするだけのもので、構造体の代入と同じだと考えればよいでしょう。クラスによっては、動的に割付けたオブジェクトを保持している場合もあれば、リスト構造や木構造になっているものもあるでしょうから、単純にメンバをコピーするだけでは不十分な場合があります。そんなときは、コピーコンストラクタを明示的に定義する必要があります。
class A
{
public:
A(const A& other)
: p_(new B(*other.p_))
{
}
private:
B* p_;
};
話を単純化するために取って付けたような例ですが、動的に割付けたオブジェクトを p_ で指すクラスAのコピーコンストラクタは、上のように、明示的にコピーコンストラクタを定義して、B型のオブジェクトを割付けなおす必要があります。なお、コピーコンストラクタの引数は、そのクラスのconst参照にしなければなりません。
コピーコンストラクタは初期化のときに呼び出されるものですが、代入の際には代入演算子が呼び出されます。これも、デフォルトでは各メンバを単純にコピーするものがコンパイラによって用意されますが、上の例のような場合には、やはり明示的に定義する必要があります。
class A
{
public:
A& operator=(const A& other)
{
B* p = new B(*other.p_);
delete p_;
p_ = p;
return *this;
}
private:
B* p_;
};
上の例からも分かるように、代入演算子を明示的に定義するときは、そのクラスのconst参照を引数として受け取り、参照を返すように定義しなければなりません。
ここで注意しなければならないのは、オブジェクトの動的割付けを行う際には、new演算子が失敗して例外を送出するかもしれないことです。もし、new演算子が例外を送出しても、元のオブジェクトが破綻しないように、代入演算子の定義は慎重に行わなければなりません。
もう一点注意しないといけないのは自己代入です。オブジェクトは、状況次第で、自分に自分を代入するような操作を行われることがあります。その場合にも破綻しないようにしなければなりません。幸いにして、例外が発生しても問題ないように実装すれば、自己代入の対策も同時にできている場合がほとんどです。
最後に、これはコピーコンストラクタと代入演算子の両方に言えることですが、オブジェクトのスライシングの問題があります。つまり、コピーコンストラクタも代入演算子も、引数はそのクラスのconst参照です。ということは、そのクラスそのものでなくても、そこから継承したクラスのオブジェクトであれば引数に指定できることを意味します。
もちろん、そのような使用方法は間違っているのですが、C++の言語仕様では、そうした操作を防ぐことができません。つまり、これについてはプログラマの責任で回避する必要があるのです。
Comment