コピーコンストラクタ
概要
コピーコンストラクタとはインスタンスの代入などを行った際に
実行される特殊なコンストラクタです。
主な処理としてはインスタンス情報を別のインスタンスにコピーします。
書式
コピーコンストラクタの書式は以下の通りです。
// 書式例:
クラス名::コンストラクタ(const クラス名& 引数名)
{
}
// 具体例:
class Character
{
public:
// デフォルトコンストラクタ
Character(int life) :
Life(life)
{
}
// コピーコンストラクタ
Character(const Character& ch)
{
}
public:
void PrintLife()
{
printf("Life:%d\n", Life);
}
private:
int Life;
};
int main()
{
Character mario(10);
Character luigi = mario;
mario.PrintLife();
luigi.PrintLife();
return 0;
}
実行結果:
Life:10
Life:不定
上記のように「クラス名&」の引数を指定したコンストラクタは
コピーコンストラクタとして扱われますが、コピーコンストラクタを定義する場合、
メンバの代入を関数内で行わないと実行結果のように不定の値となってしまうので
注意してください。
// きちんと代入を行ったパターン
Character(const Character& ch)
{
Life = ch.Life;
}
実行結果:
Life:10
Life:10
実行条件
コピーコンストラクタが実行されるケースは以下の通りです。
インスタンスに代入する |
関数に値渡しをする |
インスタンスが戻り値に使われる |
インスタンスに代入する
インスタンスにインスタンスを代入する際にコピーコンストラクタが実行されます。
class Character
{
public:
Character()
{
}
Character(const Character& ch)
{
printf("コピーコンストラクタ実行\n");
}
};
int main()
{
Character mario;
Character luigi = mario; // ここでコピーコンストラクタ実行
return 0;
}
実行結果:
コピーコンストラクタ実行
上記のようにインスタンスに代入を行うとコピーコンストラクタが実行されます。
関数に値渡しをする
関数の引数でインスタンスを値渡しするとコピーコンストラクタが実行されます。
class Character
{
public:
Character(int hp, int attack) :
Hp(hp),
Attack(attack)
{
}
Character(const Character& ch)
{
printf("コピーコンストラクタ実行\n");
}
/*
attackerにsephirothの情報をコピーするために実行される
*/
void ReceiveDamage(Character attacker)
{
Hp -= attacker.Attack;
}
int Hp;
int Attack;
};
int main()
{
Character cloud(9999, 2000);
Character sephiroth(9999, 1000);
cloud.ReceiveDamage(sephiroth); // ここでコピーコンストラクタ実行
return 0;
}
上のコードでは「cloud.ReceiveDamage」を実行した際の値渡しのタイミングで
コピーコンストラクタが実行されます。
また、コンストラクタでインスタンスを指定した場合でも実行されます。
class Character
{
public:
Character()
{
printf("コンストラクタ実行\n");
}
/*
playerインスタンスをdummyにコピーするために実行される
*/
Character(const Character& ch)
{
printf("コピーコンストラクタ実行\n");
}
};
int main()
{
Character player;
Character dummy(player); // ここでコピーコンストラクタ実行
return 0;
}
インスタンスが戻り値に使われる
最後は関数の戻り値にインスタンスが使われる場合です。
class Character
{
public:
Character(int hp, int mp) :
Hp(hp),
Mp(mp)
{
printf("コンストラクタ実行\n");
}
/*
playerインスタンスをdummyにコピーするために実行される
*/
Character(const Character& ch)
{
printf("コピーコンストラクタ実行\n");
}
/*
戻り値がインスタンスなので
*/
Character CreateCopy()
{
Character copy(Hp, Mp);
return copy; // ここでコピーコンストラクタ実行
}
private:
int Hp;
int Mp;
};
int main()
{
Character player(100, 10);
Character dummy = player.CreateCopy();
printf("\n");
player.CreateCopy();
return 0;
}
// 実行結果:
コンストラクタ実行
コンストラクタ実行
コピーコンストラクタ実行
コンストラクタ実行
コピーコンストラクタ実行
上のコードではCreateCopyのreturnでcopyインスタンスが呼び出し側に
戻される際にコピーコンストラクタが実行されています。
もし、代入を行わなかったとしても実行されることに注意してください。
問題
動的に確保したインスタンスをメンバに持つクラスの場合、
コピーコンストラクタで対策を行わないとバグが発生します。
デストラクタの復習
問題の前にまずはデストラクタの仕様を復習するため、次のコードを確認してください。
class Character
{
public:
Character(const char* name, int hp, int mp) : Hp(hp), Mp(mp)
{
strcpy_s(Name, 32, name);
printf("コンストラクタ\n");
}
~Character()
{
printf("デストラクタ\n");
}
// ゲッター
char* GetName() { return Name; }
int GetHp() { return Hp; }
int GetMp() { return Mp; }
private:
char Name[32]; // 名前
int Hp; // HP
int Mp; // MP
};
// Character内容を表示するだけ
void PrintStatus(Character ch)
{
printf("名前:%s\n", ch.GetName());
printf("HP:%d\n", ch.GetHp());
printf("MP:%d\n", ch.GetHp());
}
int main()
{
// スコープを作成してcharacterの寿命を明確にする
{
Character character("山田太郎", 50, 10);
PrintStatus(character);
}
return 0;
}
出力結果:
コンストラクタ
名前:山田 太郎
HP:50
MP:10
デストラクタ
デストラクタ
上のコードの結果、デストラクタが2回実行されていることが分かります。
これはPrintStatus関数の引数で渡した引数とmain関数のcharacterの分です。
当たり前な話ですが、引数で渡したインスタンスも関数が終了すれば
デストラクタが実行されます。
また、実行結果でコンストラクタ1回しか呼ばれていないように見えますが、
PrintStatusではコピーコンストラクタが実行されています。
値渡しの問題点
本題の問題ですが、値渡しを行った場合の動的メンバのデストラクタによる
「delete」が原因で、動的メンバのデータが壊れてしまうバグが発生します。
// 問題のコード
class Character
{
public:
Character(const char* name, int hp, int mp) : Hp(hp), Mp(mp)
{
Name = new char[32];
strcpy_s(Name, 32, name);
printf("コンストラクタ\n");
}
~Character()
{
delete[] Name;
printf("デストラクタ\n");
}
// ゲッター
char* GetName() { return Name; }
int GetHp() { return Hp; }
int GetMp() { return Mp; }
private:
char* Name; // 名前
int Hp; // HP
int Mp; // MP
};
// Character内容を表示するだけ
void PrintStatus(Character ch)
{
printf("名前:%s\n", ch.GetName());
printf("HP:%d\n", ch.GetHp());
printf("MP:%d\n", ch.GetHp());
}
int main()
{
// スコープを作成してcharacterの寿命を明確にする
{
Character character("山田太郎", 50, 10);
PrintStatus(character);
PrintStatus(character);
}
}
出力結果:
コンストラクタ
名前:山田 太郎
HP:50
MP:10
デストラクタ
名前:不定
HP:50
MP:10
エラーで停止
このエラーは1度目のデストラクタでNameが解放されて、
その後、2度目のデストラクタで解放されたNameをもう一度解放しようとして
アクセスエラーになっており、原因はコピーコンストラクタにあります。
上の絵のように値渡しではコピーコンストラクタが実行されますが、
その実行内容は当然ながらメンバ情報をそのままコピーします。
そのため、ポインタ変数の値もそのままコピーしてしまいます。
結果、1度目のデストラクタでNameが解放されてしまい、
2度目のデストラクタで解放したメモリ領域を解放しようとしてエラーになっています。
解決策
問題の解決策として「動的に確保するメンバを持たない」と
「ポインタ渡しにする」「コピーコンストラクタを変更する」があります。
動的に確保するメンバを持たない
クラス内に動的に確保するメンバを持たないようにします。
例えば今回のCharacterクラスに関してはchar型の配列を使用します。
サイズ指定を行った配列はインスタンス化された際に実体を持ちますので、
デストラクタが行われたとしても問題ありません。
class Character
{
public:
Character(char *name, int hp, int mp) : Hp(hp), Mp(mp)
{
strcpy_s(Name, 32, name);
printf("コンストラクタ\n");
}
~Character()
{
printf("デストラクタ\n");
}
// ゲッター
char *GetName() { return Name; }
int GetHp() { return Hp; }
int GetMp() { return Mp; }
private:
// 配列で名前の保存領域を確保
char Name[32]; // 名前
int Hp; // HP
int Mp; // MP
};
上のコードを実行したとしても解放処理がないので、
デストラクタが何度実行されても問題なく動作します。
ポインタ渡しにする
次は関数の引数に指定する変数をポインタにする方法です。
ポインタはあくまでアドレスを保存する変数なので、
ポインタ変数の寿命が尽きたとしてもインスタンスには影響を与えません。
// Character内容を表示するだけ
// 引数はCharacterクラスのポインタに変更
void PrintStatus(Character* ch)
{
printf("名前:%s\n", ch->GetName());
printf("HP:%d\n", ch->GetHp());
printf("MP:%d\n", ch->GetHp());
}
コピーコンストラクタを変更する
最後はコピーコンストラクタを変更します。
現状のコードはデフォルトのコピーコンストラクタなので、
メンバの値をそのままコピーしています。
そこを定義する形に変更してポインタ変数のコピーが行われないように修正します。
class Character
{
public:
// 通常のコンストラクタ
Character(const char* name, int hp, int mp) : Hp(hp), Mp(mp)
{
Name = new char[32];
strcpy_s(Name, 32, name);
printf("コンストラクタ\n");
}
// コピーコンストラクタ
Character(const Character& ch) : Hp(ch.Hp), Mp(ch.Mp)
{
Name = new char[32];
strcpy_s(Name, 32, ch.Name);
printf("コピーコンストラクタ\n");
}
~Character()
{
delete[] Name;
printf("デストラクタ\n");
}
// ゲッター
char* GetName() { return Name; }
int GetHp() { return Hp; }
int GetMp() { return Mp; }
private:
char* Name; // 名前
int Hp; // HP
int Mp; // MP
};
// Character内容を表示するだけ
void PrintStatus(Character character)
{
printf("名前:%s\n", character.GetName());
printf("HP:%d\n", character.GetHp());
printf("MP:%d\n", character.GetHp());
}
int main()
{
// スコープを作成してcharacterの寿命を明確にする
{
Character character("山田太郎", 50, 10);
// 仮引数の渡し方は変更しなくても問題ない
PrintStatus(character);
PrintStatus(character);
}
return 0;
}
実行結果:
コンストラクタ
コピーコンストラクタ
名前:山田太郎
HP:50
MP:50
デストラクタ
コピーコンストラクタ
名前:山田太郎
HP:50
MP:50
デストラクタ
デストラクタ
上記のコードのようにコピーコンストラクタを新しくメモリを確保することで
関数側のデストラクタが実行されても、呼び出し側のメモリに影響がなくなりました。