関数のオーバーライドと仮想関数


関数のオーバーライド

関数のオーバーライドとは基底クラスの関数を
派生クラスの関数で上書きすることができる機能です。

書式

オーバーライドの書式は基底クラスの関数と
全く同じ構成(名前、引数、戻り値)の関数を派生クラスに宣言します。

// 書式
class 基底クラス
{
	// オーバーライドとして使う関数
	戻り値 オーバーライド用(引数)
	{
	}
};

class 派生クラス : public 基底クラス
{
	// 基底クラスのオーバーライド用の関数と同じ構成の関数
	戻り値 オーバーライド用(引数)
	{
	}
};

// 具体例
class Ikarike
{
public:
	void Greet();
};

void Ikarike::Greet()
{
	printf("碇家の挨拶\n");
}

class Shinji : public Ikarike
{
public:
	void Greet(void);
};

void Shinji::Greet(void)
{
	printf("お、おはよう\n");
}

class Rei : public Ikarike
{
public:
	void Greet();
};

void Rei::Greet()
{
	printf("…………\n");
}

class Gendou : public Ikarike
{
public:
	void Greet();
};

void Gendou::Greet()
{
	printf("問題ない\n");
}

class Yui : public Ikarike
{
public:
	void Greet();
};

void Yui::Greet()
{
	printf("おはよう\n");
}

int main()
{
	Ikarike ikarike;
	Shinji shinji;
	Rei rei;
	Gendou gendou;
	Yui yui;

	ikarike.Greet();
	shinji.Greet();
	rei.Greet();
	yui.Greet();

	return 0;
}


実行結果:
	碇家の挨拶
	お、おはよう
	…………
	問題ない
	おはよう

上記の結果のようにIkarikeのメンバ関数Greetを
Shinji、Rei、Gendou、Yuiでオーバーライドした結果
各クラスのGreet毎に異なる結果になりました。
これが関数のオーバーライドです。

仮想関数

仮想関数とは派生クラスでオーバーライドを行うことを
前提とした基底クラスのメンバ関数のことです。
メンバ関数の宣言の前に「virtual」と記述することで、
仮想関数として認識されます。

// 書式
virtual 戻り値の型 関数名(引数);

// 具体例
class Ikarike
{
public:
	virtual void Greet(); // 仮想関数宣言
};

void Ikarike::Greet()
{
	printf("碇家の挨拶\n");
}

仮想関数として宣言した関数は後述しているポリモーフィズムの際に
効果を発揮しますが、先に純粋仮想関数について書きたいと思います。

純粋仮想関数

純粋仮想関数とは基底クラスでは内容を定義しない仮想関数のことで、
純粋仮想関数を一つでも宣言してあるクラスのことを抽象クラスと呼びます。

// 書式
virtual 戻り値の型 関数名(引数) = 0;

// 具体例
class Ikarike
{
public:
	// 純粋仮想関数
	virtual void Greet() = 0;
};

class Shinji : public Ikarike
{
public:
	virtual void Greet();
};

void Shinji::Greet()
{
	printf("お、おはよう\n");
}

int main()
{
	Shinji shinji;
	shinji.Greet();
	
	return 0;
}

実行結果:
	お、おはよう

純粋仮想関数はvirtual設定をした関数の最後に「= 0」をつけます。
今回はIkarikeクラスのGreet関数を純粋仮想関数にしました。

純粋仮想関数の特性

純粋仮想関数は次の二つの特性を持っています。
この特性から純粋仮想関数は「派生先で必ずその仮想関数を定義している」ことが
約束されるので、基底クラスを作成する際にはよく利用されています。

抽象クラスの特性
インタスタンス化できない
継承先の仮想関数の定義の義務化

インタスタンス化できない

抽象クラスは純粋仮想関数を持っているという性質上インスタンス化できません。
これは定義できない関数を持っているクラスをインスタンス化できてしまうと
間違って仮想関数を呼び出した際にエラーになることを防ぐためです。

// 具体例
class Ikarike
{
public:
	// 純粋仮想関数
	virtual void Greet() = 0;
};

int main()
{
	Ikarike ikarike;	// 純粋仮想関数が宣言されているのでエラー
}

継承先の仮想関数の定義の義務化

純粋仮想関数を宣言したクラスを継承した派生クラスは
必ず仮想関数を実装しなければいけません。
これで、実装を行わないとエラーになるので派生先で定義し忘れる心配はありません。

// 具体例
class Ikarike
{
public:
	// 純粋仮想関数
	virtual void Greet() = 0;
};

class Shinji : public Ikarike
{
public:
	virtual void Greet();
};

void Shinji::Greet()
{
	printf("お、おはよう\n");
}

class Rei : public Ikarike
{
};

int main()
{
	Shinji shinji;
	Rei rei;		// Greetを定義していないのでエラー
	shinji.Greet();
	
	return 0;
}

ポリモーフィズム

ポリモーフィズムとは同じ関数を呼び出しているのに
その関数が異なる動作を行う性質のことをいいます。
この性質を上手く利用することで、プログラミングの効率化が図れます。
また、ポリモーフィズムは多態性(たたいせい)多様性とも呼ばれています。

// ポリモーフィズムの例
class Ikarike
{
public:
	virtual void Greet() = 0;
};

class Shinji : public Ikarike
{
public:
	virtual void Greet();
};

void Shinji::Greet()
{
	printf("お、おはよう\n");
}

class Rei : public Ikarike
{
public:
	virtual void Greet();
};

void Rei::Greet()
{
	printf("…………\n");
}

class Gendou : public Ikarike
{
public:
	virtual void Greet();
};

void Gendou::Greet()
{
	printf("問題ない\n");
}

class Yui : public Ikarike
{
public:
	virtual void Greet();
};

void Yui::Greet()
{
	printf("おはよう\n");
}

int main(void)
{
	Shinji shiji;
	Rei rei;
	Gendou gendou;
	Yui yui;

	Ikarike* family[] =
	{
		&shiji, &rei, &gendou, &yui
	};

	for (Ikarike* human : family)
	{
		human->Greet();
	}
}

実行結果:
	お、おはよう
	…………
	問題ない
	おはよう

上記のコードのようにIkarikeのGreetを仮想関数にしておき、
派生先のクラスごとに関数を定義します。
そして、それらのクラスをインスタンス化し、Ikarikeの配列に保存します。
あとは、その配列の要素でGreetを呼び出すだけで、各クラスのGreetの挙動になります。

このように、呼び出し側が複数のクラスを一括で管理し、同じ関数を呼び出すだけで、
派生先の関数が実行されるので、呼び出し側は各クラスごとのインスタンスを使用して
Greet等の関数を呼び出さなくて済みます。
※ポリモーフィズムを使用していないパターンはこのページの
 最初のサンプルコードがそれにあたります。