コピーコンストラクタ

■クラスを引数で使用

関数の引数にクラスのインスタンスを設定することができます。

●コード例:
	class Character
	{
	public:
		Character(char *name, int hp, int mp) : m_Hp(hp), m_Mp(mp)
		{
			strcpy_s(m_Name, name);
			printf("コンストラクタ\n");
		}
			
		~Character()
		{
			printf("デストラクタ\n");
		}
				
		// ゲッター
		char *GetName() { return m_Name; }
		int GetHp() { return m_Hp; }
		int GetMp() { return m_Mp; }
			
	private:
		char m_Name[32];	// 名前
		int m_Hp;		// HP
		int m_Mp;		// MP
	};

	// Character内容を表示するだけ
	void PrintStatus(Character ch)
	{
		printf("名前:%s\n", ch.GetName());
		printf("HP:%d\n", ch.GetHp());
		printf("MP:%d\n", ch.GetHp());
	}

	void main(void)
	{
		// スコープを作成してcharacterの寿命を明確にする
		{
			Character character("山田太郎", 50, 10);

			PrintStatus(character);
		}
		while (1);
	}

	出力結果:
		コンストラクタ
		名前:山田 太郎
		HP:50
		MP:10
		デストラクタ
		デストラクタ

上の例で関数にクラスのインスタンスを渡せることが証明できましたが、
デストラクタが2度行われていますがこれはバグではなくデストラクタの仕様です。

●デストラクタが呼び出される条件
	デストラクタが呼び出されるのはインスタンスの寿命や動的解放の時です。
	これは変数として存在できるスコープの外に処理が進んだ時や
	delete関数を使用してインスタンスを解放した時が
	インスタンスの寿命が尽きるタイミングとなります。

●デストラクタの対象となった変数
	上の例でデストラクタの対象になっているのはPrintStatus関数の引数「ch」と
	main関数で宣言した変数「character」の2つです。
	どちらもスコープ外に出たことによる寿命です。

●コンストラクタについて
	コンストラクタが2度呼ばれていないのは関数呼び出しの引数に対して
	クラスのコンストラクタが起動したら、引数に渡したクラスのデータが
	初期化されてしまうからです。
	それを防ぐためにクラスのインスタンスを引数で渡したとしても
	コンストラクタは呼ばれません。

■クラスの値渡しの問題点

上の項目でクラスの値渡しではデストラクタが2度動作することが分かりました。
この挙動で問題となるのが、デストラクタでの「delete」を行っている場合です。
メンバ変数にポインタ変数を持っており、クラスが動作している中で動的に確保を行い、
デストラクタで「delete」を行い、解放するケースがありますが
今回の値渡しでは、この「delete」処理が問題となります。

●コード例
	class Character
	{
	public:
		Character(char *name, int hp, int mp) : m_Hp(hp), m_Mp(mp)
		{
			m_Name = new char[32];
			strcpy_s(m_Name, 32, name);
			printf("コンストラクタ\n");
		}
			
		~Character()
		{
			delete[] m_Name;
			printf("デストラクタ\n");
		}
		
		// ゲッター
		char *GetName() { return m_Name; }
		int GetHp() { return m_Hp; }
		int GetMp() { return m_Mp; }
			
	private:
		char *m_Name;			// 名前
		int m_Hp;			// HP
		int m_Mp;			// MP
	};

	// Character内容を表示するだけ
	void PrintStatus(Character ch)
	{
		printf("名前:%s\n", ch.GetName());
		printf("HP:%d\n", ch.GetHp());
		printf("MP:%d\n", ch.GetHp());
	}

	void main(void)
	{
		// スコープを作成してcharacterの寿命を明確にする
		{
			Character character("山田太郎", 50, 10);

			PrintStatus(character);
			PrintStatus(character);
		}
	
		while (1);
	}

	出力結果:
		コンストラクタ
		名前:山田 太郎
		HP:50
		MP:10
		デストラクタ
		名前:不定
		HP:50
		MP:10
		デストラクタ
		エラーで停止

	上の例でm_Nameを配列からポインタに変更してコンストラクタで動的確保、
	デストラクタで動的解放を行うように処理を変更し、
	PrintStatusも2度呼び出すようにしています。
	その結果、2度目のPrintStatus呼び出してエラーで停止しました。
	停止の理由はm_Nameに不定な値が入っているからです。
	1度のPrintStatusの終了後にm_Nameが解放されてしまうので
	2度目のPrintStatusのm_Nameを出力するPrintfでは不定な値を参照して停止します。

	cpp_0007

●解決策
	問題の解決については以下のような解決策があります。

	・動的に確保するメンバを持たない
		クラス内に動的に確保するメンバを持たないようにします。
		例えば今回のCharacterクラスに関しては元のchar型の配列に戻します。
		サイズ指定を行った配列はインスタンス化された際に実体を持ちますので、
		デストラクタが行われたとしても問題ありません。

		例:
			class Character
			{
			public:
				Character(char *name, int hp, int mp) : m_Hp(hp), m_Mp(mp)
				{
					strcpy_s(m_Name, 32, name);
					printf("コンストラクタ\n");
				}
				~Character()
				{
					printf("デストラクタ\n");
				}
				
				// ゲッター
				char *GetName() { return m_Name; }
				int GetHp() { return m_Hp; }
				int GetMp() { return m_Mp; }
			private:
				// 配列で名前の保存領域を確保
				char m_Name[32];		// 名前
				int m_Hp;			// HP
				int m_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());
			}

	・コピーコンストラクタを使用する
		コピーコンストラクタについては次の項目で説明します。

■コピーコンストラクタ

今回の問題の内容は以下の通りです。
	・値渡しは新しいメモリに作成された領域に値がコピーされる
	・クラスの実体を渡した場合もメンバ変数の値が新しい領域にコピーされる
	・クラスの実体を渡した場合は関数終了時にデストラクタが呼ばれる
	・クラスに動的確保のメンバがあった場合かつデストラクタで解放を行っている場合
	 動的確保したメンバが関数終了時のデストラクタで解放される

動的確保したメンバに対して実引数と仮引数で関係が残ってしまっているのが問題なので、
仮引数側で動的メンバに対して確保を行ってもらえば問題は解決します。
仮引数でもコンストラクタが呼び出されれば問題ありませんが、
このページの一番最初の例で証明したとおり仮引数のクラスのコンストラクタは動作しません。
ただ、これは通常のコンストラクタが動作しないだけで、
オブジェクトのコピーを作成するための特殊なコンストラクタは呼び出されます。
このコンストラクタをコピーコンストラクタと呼びます。

●書式
	書式例:
		クラス名::コンストラクタ(クラス名 &引数名)

	具体例:
		Character::Character(Character &ch)

●使用方法
	コピーコンストラクタでメモリの動的な確保を行い、
	そこに仮引数のクラスのデータをコピーします。

	例:
		Character::Character(Character &ch) : m_Hp(ch.GetHp()), m_Mp(ch.GetMp())
		{
			m_Name = new char[32];
			strcpy_s(m_Name, 32, ch.GetName());
			printf("コピーコンストラクタ\n");
		}

●サンプルコード
	class Character
	{
	public:
		// 通常のコンストラクタ
		Character(char *name, int hp, int mp) : m_Hp(hp), m_Mp(mp)
		{
			m_Name = new char[32];
			strcpy_s(m_Name, 32, name);
			printf("コンストラクタ\n");
		}
			
		// コピーコンストラクタ
		Character::Character(Character &ch) : m_Hp(ch.GetHp()), m_Mp(ch.GetMp())
		{
			m_Name = new char[32];
			strcpy_s(m_Name, 32, ch.GetName());
			printf("コピーコンストラクタ\n");
		}

		~Character()
		{
			delete[] m_Name;
			printf("デストラクタ\n");
		}

		// ゲッター
		char *GetName() { return m_Name; }
		int GetHp() { return m_Hp; }
		int GetMp() { return m_Mp; }
	private:
		char *m_Name;		// 名前
		int m_Hp;			// HP
		int m_Mp;			// MP
	};

	// Character内容を表示するだけ
	void PrintStatus(Character character)
	{
		printf("名前:%s\n", character.GetName());
		printf("HP:%d\n", character.GetHp());
		printf("MP:%d\n", character.GetHp());
	}

	void main(void)
	{
		// スコープを作成してcharacterの寿命を明確にする
		{
			Character character("山田太郎", 50, 10);

			// 仮引数の渡し方は変更しなくても問題ない
			PrintStatus(character);
			PrintStatus(character);
		}

		while (1);
	}

	上記のサンプルの通り、クラスを定義した側がコピーコンストラクタを用意するだけで
	使用側が何かを変える必要はありません。
	これで問題なくクラスを引数として使用することができるようになりました。