クラスインスタンスをコピーする - C++ プログラミング
PROGRAM
クラスインスタンスをコピーする
既定のコピーと問題点
C++ のクラスは、代入演算子「=」を使うことでクラスインスタンスを複製できます。
CMyClass class1;
CMyClass class2;
class2 = class1;
たとえば、CMyClass 型の変数 class1 と class2 があったとき、このように代入演算をするだけで、class1 の値が class2 に複製されます。
変数の宣言と合わせて代入することもできます。
CMyClass class1;
CMyClass class2 = class1;
ちなみにこれらの操作で複製される値は、そのクラスが持っている属性の値です。
ここで注意したいことがあって、複製されるのはあくまでも属性に格納されている値そのものです。
そのため、ポインタはそのまま同じ値が複製先のクラスに設定されることになるのですが、これによってコピー元もコピー先も、動作に支障をきたすことがよくあります。
たとえば、クラス内で属性に値を保持するのに new 演算子を使ってメモリを割り当てていたとします。
この属性を既定のコピーで複製すると、同じメモリをコピー元とコピー先の両方で参照することになります。
この場合、たとえばどちらかのクラスでその属性が指し示すメモリ内のデータを書き換えたりすると、同じメモリを参照している両方のインスタンスの値が変わってしまうことになります。
また、クラス内で new 演算子を使って確保しているのであれば、インスタンスが解放されるときに delete するのが普通でしょうから、どちらかのインスタンスが解放されたタイミングで、もう一方のインスタンスでそのメモリを参照できなくなってしまいます。
そういった細かい事情は、そのクラスを制作したプログラマにしかわからないところなので、既定で用意される代入演算では C 構造体と同じようなコピーしかできないのは、仕方のないところです。
そこで C++ のクラスでは、そういったコピーの細かい動作を自分で実装することができるようになっています。
独自のコピーメソッドを用意する
C++ でクラスインスタンスをコピーする方法の 1 つとして、まず、自分自身で独自のコピーメソッドを用意する方法が考えられます。
void copy(const CMyClass& myClass);
このようなプロトタイプと、たとえば次のような実装を用意します。
ここで、複製するときに使用している m_serial というのは、private で宣言された char* であるとします。
void CMyClass::copy(const CMyClass &myClass)
{
delete [] m_serial;
if (m_serial != NULL)
{
m_serial = new char[strlen(myClass.m_serial) + 1];
strcpy(m_serial, myClass.m_serial);
}
else
{
m_serial = NULL;
}
return *this;
}
引数で受け取ったクラスインスタンスの private 属性 m_serial に直接アクセスしていますけど、これは、自分自身と同じクラスの private メンバであれば、違うインスタンスのものでも直接アクセスできるためです。
このようにして、引数に渡されたインスタンスの属性を、別のメモリに複製してから自分自身に持たせることで、コピー元に左右されることなく、コピー元に干渉することなく、同じ値を持つインスタンスとして複製することができました。
ただし、このように実装したプログラムは自分自身で呼び出さないといけないので、使用する場合は次のようになります。
CMyClass class1;
CMyClass class2;
class2.copy(class1);
代入演算子を使って代入したときは、標準のコピーが行われて m_serial に同一のアドレスが設定されてしまう(両方が同じメモリを参照してしまう)ことになるので、間違えて代入しないように十分注意が必要になります。
代入演算子をオーバーロードする
クラスインスタンスの代入演算によるコピーの動作は、そのクラスで代入演算子をオーバーロードすることで調整できます。
CMyClass& operator=(const CMyClass& myClass);
このように代入演算子をオーバーロードすることで、このインスタンスに対して代入されたときの処理を定義できます。
今回は同じクラスのインスタンスをコピーするのが目的なので、引数には自分自身と同じクラスの参照を受け取るように定義してあります。
このメソッドの中で、引数で受け取ったインスタンスを自分自身に複製してあげます。
先ほど作成した copy メソッドを使って実装すると、次のような感じになります。
CMyClass& CMyClass::operator=(const CMyClass &myClass)
{
this->copy(myClass);
return *this;
}
戻り値として CMyClass& を返しているのは、代入後に引き続きドット演算子を使ってアクセスできるようにするという、慣例的なもののようです。
while の条件式の中で、代入文を使いつつ判定するという組み方もされたりするので、そういったときにも必要な配慮になるようでした。
ともあれこのようにすれば、次のような代入文を実行するだけで、今回実装したコピー処理が呼び出されるようになります。
CMyClass class1;
CMyClass class2;
class2 = class1;
ただし、変数宣言と合わせて代入文を使ったときには、代入演算子の処理は呼び出されないので注意が必要です。
コピーコンストラクタを実装する
変数宣言と合わせて代入文を使った場合は、代入演算子ではなくコピーコンストラクタが呼び出されます。
コピーコンストラクタというのは、自身と同じ型のインスタンス参照を引数に取るコンストラクタのことで、引数で受け取ったインスタンスの値を複製して新しいインスタンスを生成するために使用します。
コピーコンストラクタのプロトタイプ宣言は次のように、コンストラクタの引数として、同じ型の参照を const で取るように定義します。
CMyClass(const CMyClass& myClass);
実装は、代入演算子のときとほとんど同じですけど、属性の値が初期化されていない状態で呼び出されるので注意します。
CMyClass::CMyClass(const CMyClass &myClass)
{
m_serial = NULL;
this->copy(myClass);
}
このようにしておけば、変数宣言と合わせて代入文を実行するだけで、今回実装したコピーコンストラクタが呼び出されます。
CMyClass class1;
CMyClass class2 = class1;
こちらはコンストラクタなので、インスタンス構築後の代入文では呼び出されないので注意が必要です。
まとめ
このように、C++ では代入文を使ってインスタンスを複製できるようになっています。
C++ でインスタンスを複製するときの特徴としては、
- クラスに何も実装しない場合は、属性値がそのままの値でコピーされる。(シャローコピー)
- 代入演算子で任意のコピー処理を行う場合は、コピーコンストラクタの実装と、代入演算子のオーバーロードの 2 つを実装する。
といった辺りに注意して実装する形になります。
ちなみに、コピーコンストラクタや代入演算子の引数には、自分自身のクラス型が指定されます。
そのため、派生クラスも渡すことができますけど、派生クラスを渡しても、コピー先はあくまでも自分自身のクラスそのものなので、いくら派生クラスが渡されても、引き継がれるのは自分自身に実装されている属性だけになります。
[ もどる ]