今回もちょっとWindowsから離れるのだ。いや、結局は、Windowsの話になるのだが…。実は、ず〜〜っと前から疑問であって、有耶無耶に覚えていた仮想関数って事に、メスを入れたいと思うのだ。別に、無視していた訳ではないのだが、例えば、『デストラクタは、仮想にする。こうするのが、当然』として片付けてしまっていたから、何故か??って事については考えてなかったのだ。但し、これを書き始めてから1ヶ月も経っていない自分が、この疑問に正確で、分かりやすい回答を出せるかは疑問なのである。最終的には、ここに追記するなりして、正解を導き出すつもりで居るが、最初の内は、間違った事を書くかも知れないので、ご容赦を!
- 実体が無くても良い
仮想関数というのは、実体が無くても良いのである。即ち、宣言だけが存在する(但し、値は返す…こういうものを抽象関数(純粋仮想関数)と言う)か、若しくはダミーの関数が宣言されているのである。勿論,有効な機能を持つ関数が有っても良いが、基本的には、継承して拡張される為のものなので、ダミーか抽象関数にするのが普通である。なぜ、こうしなければならないか?という事については、後で、まとめて説明する。
- クラスの中でしか使えない?
仮想関数は、クラスの中でしか使えないのである。単独で宣言したら、コンパイラがいっぱいエラーを出していたので、駄目だな!と諦めた。多分,クラスの構造を考えれば、メンバ関数の実体は、そのものがクラスの構造体の中に存在するのでは無く、メンバ関数へのポインタという形で組み込まれている…と考えれば、この様な関数が、存在を許される事も納得出来るのでは無いだろうか?当然,ダミーの関数なので、単独でインスタンス化して、実行しても、殆ど意味がないのである。これはつまり、メモリの無駄使いなのである。何故こうするのか?という事については、後から説明したい。
- どの様に書けば良いのか?
簡単である。クラスを構成しているメンバ関数の頭に、virtualを付ければ良い。
例)
virtual int test(void) ;
→この様に宣言したら、一応ダミーでも、メンバ関数(実体)を記述する。
若しくは、
virtual int test(void)=0 ;
→返す値だけ記述する。これが抽象関数
- なぜ、この様な関数が必要なのか?
ここが重要なのである。仮想関数は、『継承される事を目的として考えられた関数の持ち方なのだ』と考えれば、自分なりに納得のいく説明になりそうなのだ。即ち、基本となるクラスを作成する時、『このクラスは、継承されて、この様なメンバ関数が追加されるべきだ!』という事を、最初に宣言してしまうのである。この様に言うと、『追加されるべきだ!と考えるなら、最初から書いておけば良いじゃん』と言う疑問が出てくるだろう。ここが説明し辛いのである。元々この疑問には、2通りの回答が有ると思うのだ。
- 複数の形状に継承されると考える場合
例えば、VisualC++でMFC等を使った事が有る人は見覚えが有ると思うが、CFrameWndやCWinAppクラスを継承して、CMainWindowやCMainApp等というクラスを生成したりするのである。このクラスは、アプリを造る度に、違った名前や、違う組織構成となっているのだが、OnPaint()やInitInstance()等というメンバ関数は、必ず実体付きで書いていた…という記憶は無いだろうか?これが、基本クラスに於いて、仮想関数として宣言されているとしたらどうだろう?(ここは推測で言っている)
当然ここは、ユーザが、自分のアプリに併せて造り込む訳だから、複数の形状に拡張される訳である。この様な場合、少なくとも、OnPaint()とかInitInstance()は、派生クラスにて、上書きされるものとして、仮想関数である必要が有るのだ。(MFCでもそうなっていると思う…多分)
- 基本クラスの関数をそのまま使用して、更に拡張して使いたい場合
MFCの場合もそうだが、WinMainやWindowProcはユーザから隠蔽されていて、結局のところ、MainWindowを登録したり、メッセージのハンドルをしているのは、MFCクラス側なのである。この様な隠蔽された部分で、ユーザの造ったクラス(例えばCMainWindow)を、
CMainWindow *cMwin ;
等と宣言して使ってくれるだろうか?いや、そもそも、こんなクラスをユーザが造るとは知らないクラスライブラリ側は、ユーザ定義のクラス名も、そのメンバ関数についても知らない訳である。だから実際は、多分(ここからも推測)
CFrameWnd *cMwin ;
cMwin=new CMainWindow() ;
の様に、CFrameWndクラス型のポインタを宣言して、これを、CMainWindow等という、ユーザクラス型にインスタンス化して使っているのである…多分(てゆーか、自分が造るならそうする)
こんな事出来るの???と、その昔,自分も驚いたのだが、基本クラスを継承して、新しい派生クラスを造った場合に限り、この様に、『基本クラスの宣言をして、派生クラス型にインスタンス化する』という事が出来るのである。こうする事の利点は、基本クラスの機能は、そのまま生きるという事である。つまり、インスタンスは、派生クラスでも、基本クラスの型で宣言されている訳だから、『このインスタンスは、基本的に、基本クラスとして動作するのである』(ややこしいな!?)つまり、クラスライブラリ側としては、ユーザが定義する部分を仮想関数(抽象関数)としておいて、それを含めた基本クラスのメンバ関数だけで、クラスライブラリが構築出来てしまうのである。ユーザは、このクラスを継承して、仮想関数となっている、OnPaint()やInitInstance()等を拡張すれば、様々な形のWindowsアプリが出来る訳である。つまり、CFrameWnd単体で動作させた場合は、OnPaint()は何もしなかったのであるが、ユーザが拡張する事によって、ユーザの作成したWindowsをペイントする様になる訳である。
ぐだぐだ語ったが、要は、クラスライブラリの様な、基本クラスを造っていくには非常に都合が良い仕組みなのである。逆に、この仕組みが無ければ、クラスライブラリというのは成り立たなくなってしまうのである。因みに、それぞれの場合のアクセス許容範囲は、
- 基本クラス型で宣言され、派生クラス型でインスタンス化
ユーザプログラム→基本クラスのメンバ関数 … ○アクセス可
ユーザプログラム→派生クラスのメンバ関数 … △一部アクセス可
オーバライドされているものだけ、派生クラス側のメンバ関数が呼ばれる。
全く別に造られたメンバ関数にはアクセス不可
- 派生クラス型で宣言され、派生クラス型でインスタンス化
ユーザプログラム→基本クラスのメンバ関数 … ○アクセス可
ユーザプログラム→派生クラスのメンバ関数 … ○アクセス可
になるのである。これを見ると、後者の方が優れている様に思えるかも知れないが、様々な形に拡張される基本のクラスを作成する人にとっては、ここは、△の方が都合が良いし、ユーザ側としても、基本クラスの一部を拡張するだけで済むのであるから、これ程好都合な事は無いのである。…この辺、オブジェクト指向なんだな。
ここまでで、派生クラスの使われ方と、どの様な場面で仮想関数が必要かという事については何となくでも見えて来ただろうか?勿論,『基本クラスと派生クラスが全く別物で、これで完結するクラス』という場合には、『派生クラス型で宣言され、派生クラス型でインスタンス化』されたものを使えば良いし、仮想関数も使わなくて良いのである。
- 仮想関数が実体を持つとどうなる?
基本的に、仮想関数は、実体を持たなくても良い関数なので、実体が有っても良いのである。単独で使われれば、動作させる事も可能であるが、派生クラスに、同名のメンバ関数が有れば、これはオーバーライド(上書きされるイメージ)されるので、派生クラスのメンバ関数が生きる事になる。但し、いくらオーバーライドされたとは言え、関数として実体は存在しているので、派生クラス側から、この実体の有る仮想メンバ関数を呼び出す事も可能なのである。また、『基本クラス,派生クラス共に実体の有る仮想関数』というのも有りである。ただ、今後、更に継承される予定が無ければ、その関数は仮想関数にしない方がスッキリするのでは無いだろうか?
- デストラクタ
そもそも、その昔、読んだVisualC++に関する書籍に、後のサンプルで試す内容と、逆の事が書かれていたのが、理解の妨げになっていたのだ。その書籍には、継承する側のデストラクタが、仮想になっていれば良い。みたいな書き方をされていて、どう考えても、つじつまが合わなかったのだ。実は、そこに書いてあったサンプルを鵜呑みにしたのがいけなかったのだけど、今回試してみて、やっと、その書籍の間違いだって気付いたよ。何事も、自分で試さないと分からないよね。そんなこんなで、デストラクタについては、理解に苦しんだのだ。何度、考えるの止めようと思ったか分からないね〜。仮想関数は、派生クラスに上書きされると書いたが、デストラクタだけは、ちょっと勝手が違うのである。『同名では無いので上書きはされないが、インスタンス化された時は同名になるので、両方とも呼び出される』(ここら辺がやたらとややこしいのだ…)のである。逆に、基本クラスのデストラクタが仮想で無い場合、こちらが生きてしまうのである…なぜだろう??
ここで、下の例の様に考えれば、ちょっと納得いかない?
例)基本クラス,
class ClassA
と、これを継承した、
class ClassB : public ClassA
が有った場合,
ユーザプログラムで、
ClassA *ca ;
と、最初にクラスの形を宣言された時点で、そのクラスの
デストラクタは~ClassA()
と決まってしまうのだ。
そこに、
ca=new ClassB() ;
の様に、別の形でインスタンス化が為された場合,これは、ClassBのコンストラクタであるからそのまま実行され、基本クラスのClassAコンストラクタも勿論実行される。ここまでは、良いのである。問題はデストラクタで、この時点で、ClassBのデストラクタは、元々の宣言がClassA型であるから、~ClassA()に名前が変わるのだが、基本クラスのデストラクタが仮想関数になっていると、『同じ名前になるけど、元々の名前が違うので上書き出来ない』という状況になるのだ。その為、ClassAのデストラクタと、ClassBのデストラクタが同名で存在する様になり、
delete ca ;
を行うと、両方が呼び出される訳である。
ここで、基本クラスのデストラクタが仮想関数になっていない場合は、どうなるかと言うと、『同じ名前にしたいけど、上書き出来ないから、名前が変更出来ない』という状況になって、ClassBのデストラクタは、~ClassB()のまま、居続けてしまうのである。この状況で、
delete ca ;
を行うと、本来のデストラクタである~ClassA()だけが呼ばれ、~ClassB()は呼ばれなくなってしまう訳だ。これでは、ClassBにて新たに取得した部分のメモリも解放出来ないというバグになってしまう。なんだかややこしいし、設計ミスの様な気もする今日この頃なのだが、基本クラスのデストラクタは、仮想関数にしておけ!と言うのは、こういう理由からなのである。確かに、仮想関数にしなくても良い場合は存在するのだが、何れにしても動作は変わらないので、仮想関数にしておいた方が無難…というのが結論だろうか?
- サンプル
サンプルを幾つか用意してみた。サンプルの先頭に、内容を簡単に書いたので、本編の内容と照らし合わせて参照してほしい。
テストサンプル015
|