仮想関数


関数のオーバーライド

一般に基本クラスは汎用的な情報しか含みません
そのため、基本クラスの機能を拡張してく形で派生クラスを建築します

このとき、基本クラスのメンバ関数を再定義することができます
このような関数を仮想関数と呼びます
仮想関数は、基本クラスで再定義可能であるということを明示する必要があります
仮想関数は次のようにvirtual宣言します

virtual member-function-declarator

member-function-declarator は、メンバ関数を宣言します
通常の宣言の頭に virtual を付けただけですね

virtual キーワードは基本クラスのみで指定します
派生クラスでの再定義の時は virtual は不要です
仮想関数の呼び出しは、通常のメンバ関数と代わりません

仮想関数による再定義は、通常のオブジェクト操作ではなんの影響もありません
これは、前回行ったポインタへの代入で大きな威力を発揮します
ポインタが仮想関数を呼び出した場合、オブジェクト型のよって呼び出す関数が決定します
#include <iostream>
using namespace std;

class Kitty {
public:
	virtual void paint() { cout << "Kitty on your lap\n"; }
} obj1 ;

class Chobits : public Kitty {
public:
	void paint() { cout << "Chobits\n"; }
} obj2 ;

class Di_Gi_Gharat : public Chobits {
public:
	void paint() { cout << "Di Gi Gharat\n"; }
} obj3 ;

int main() {
	Kitty *po1 = &obj1 , *po2 = &obj2 , *po3 = &obj3;

	po1->paint();
	po2->paint();
	po3->paint();

	return 0;
}
基本クラス Kitty のメンバ関数 paint() は仮想関数です
このクラスの派生クラスでは、この関数を再定義することができます

Kitty を基本クラスとする Chobits 及びそれの派生クラス Di_Gi_Gharat は
paint() 関数を独自に再定義しています
再定義されたメンバ関数は、ポインタから呼び出された時オブジェクト型に合わせて呼び出します
最大の特徴は、この判断をコンパイル時ではなく実行時にしているという点です

仮想関数による動的なポリモーフィズムをオーバーライドと呼びます
オーバーロードはコンパイル時の静的なポリモーフィズムでしたが
オーバーライドは実行時に決定される動的で強力なポリモーフィズムなのです
また、仮想関数を含むクラスをポリモーフィッククラスと呼びます
#include <iostream>
using namespace std;

class Chobits {
public:
	virtual void paint() { cout << "Chobits\n"; }
} obj1 ;

class Di_Gi_Gharat : public Chobits {
public:
	void paint() { cout << "Di Gi Gharat\n"; }
} obj2 ;

int main() {
	Chobits *po;
	unsigned char ch;

	cout << "ちぃ? y/n >";
	cin >> ch;

	if (ch == 'y') po = &obj1;
	else po = &obj2;

	po->paint();
	return 0;
}
このプログラムは、代入されるオブジェクトによって
実行するメンバ関数を動的に変化させているプログラムです

入力要求に対して 'y' を入力すると Chobits クラスの paint() が呼び出され
そうでなければ Di_Gi_Gharat の paint() が呼び出されます

仮想関数は、必ずしも再定義しなければならないものではありません
必要がなければ再定義しなくても良いのです
その場合、再定義していないオブジェクト型は最後に再定義したクラスのメンバ関数を参照します
これまで継承したクラスの中で、再定義しているクラスがなければ
virtual を宣言した基本クラスの最初の仮想メンバ関数を参照します
#include <iostream>
using namespace std;

class Kitty {
public:
	virtual void paint() { cout << "Kitty on your lap\n"; }
};

class Chobits : public Kitty {
public:
	void paint() { cout << "Chobits\n"; }
} obj1 ;

class Di_Gi_Gharat : public Chobits {
} obj2 ;

int main() {
	Kitty *po = &obj2;

	po->paint();
	return 0;
}
Di_Gi_Gharat クラスは、paint() メンバ関数を再定義していません
main() 関数でのこのクラスのオブジェクトを指す Kitty 型ポインタ変数から
paint() メンバ関数を呼び出しています

この場合、結果は Chobits クラスの paint() が呼び出されます
もし、Di_Gi_Gharat クラスが Kitty クラスを拡張している場合は、Kitty クラスの paint() が呼び出されます

最後に、少しネイティブな動作について説明します
通常の関数やフレンド関数、メンバ関数などはコンパイル時にアドレスが判明します
これらは、コンパイル時点でそれぞれの関数を呼ぶアドレス情報が確定されるため
関数の呼び出しにかかるオーバーヘッドが少なく、非常に効率が良いです
このように、コンパイル時点で確定している情報をコンパイル時バインディングと呼びます

コンパイル時バインディングは、高速に動作しますが柔軟性に欠けます
似たような動作でも、異なるイベントとしてソースコードを記述しなければいけません

これに対し、オーバーライドのようなオブジェクト指向における
実行時に決定される情報を実行時バインディングと呼びます
オーバーヘッドが大きくなりますが、非常に高い柔軟性があります

仮想関数は実行時バインディングになります
どのオブジェクト形を実行するかは、実行しなければわかりません
これは非常に柔軟なプログラムを欠けますが、反面オーバーヘッドが大きいことを忘れないでください


デストラクタの憂鬱

仮想関数が唯一思うように動いてくれない場面があります
これを知らなければ、設計時に思いがけない障害に突き当たることになるでしょう

デストラクタは、派生クラスから基本クラスへ向かって順番に呼び出されます
すなわち、派生クラスから順にデータを解放していくことを表します
もし、デストラクタが仮想関数を呼び出した場合
派生クラスの情報は、すでに崩壊している可能性があるのです

そのため、C++ 言語のデストラクタはオーバーライドを行いません
デストラクタで仮想関数を呼び出しても、実体関数にアクセスせずに
デストラクタが発生しているクラスの仮想関数を呼び出すのです
これは、参照回数などを管理する特殊な構造を設計するときに注意する必要があります
#include <iostream>
using namespace std;

class Test1 {
public:
	virtual void func() { cout << "Kitty on your lap\n"; }
	~Test1() { this->func(); }
};

class Test2 : public Test1 {
public:
	void func() {
		cout << "Tokyo mew mew\n";
		Test1::func();
	}
} obj ;

int main() { return 0; }
このクラスの func() 関数は仮想関数です
Test1 クラスを継承する Test2 クラスのインスタンス obj が破壊されると
最終的に ~Test1() が呼び出され、このとき仮想関数 func() が呼び出されています

Test2 クラスのインスタンスが ~Test1() を呼び出しているため
一見すると Test2::func() が呼び出されることを期待しますが、そうはならないのです
このプログラムは Kitty on your lap という文字だけを表示して終了します



前のページへ戻る次のページへ