Version 18.08
継承で機能を分けて再利用
「前々回と前回は、継承を使って機能を分けたプログラムを見てもらいまし
た」
『でもプログラム見ただけだよね』
「というわけで、今回は前回のプログラムの仕組みについて解説します。ま
ず、プログラムが書かれたソースファイルとヘッダーファイルは以下の5つ
になります」
・CDialog クラス
Dialog.cpp
Dialog.h
・CNewCalcDialog クラス
NewCalcDialog.cpp
NewCalcDialog.h
・WinMain() 関数
Main.cpp
『つまり全部で2つのクラスと1つの関数があるわけね』
「その2つのクラスは以下のように分けられています」
・CDialog クラス
ダイアログの基本機能を持つクラス。
ダイアログを作成する DoModal() メンバ関数を持つ。
メッセージのディスパッチをする DispatchDialogProc() メンバ関数を持つ。
純粋仮想関数 DialogProc() メンバ関数を持ち、
DispatchDialogProc() メンバ関数から呼び出される。
・CNewCalcDialog クラス
ボタンが押されたときの処理等を行う「本当のダイアログプロシージャ」を
持つクラス。
CDialog クラスの派生クラス。
DialogProc() メンバ関数をオーバーライド(正確には実装)している。
この中でイベント処理を行っている。
「つまり、ダイアログとして必要不可欠な機能を持つ CDialog クラスと、
イベント処理を行う CNewCalcDialog クラスと、目的によってクラスが分か
れているわけです」
『分ける理由は?』
「簡単に言えば〈共通化できる部分とカスタマイズする部分〉に分けた、
ってところかな」
『共通化とカスタマイズ?』
「まず、 CDialog クラスには以下の機能があります」
・API の DialogBox() 関数でダイアログを作る機能
・最初にダイアログプロシージャが呼ばれたら、ウィンドウの 32 ビット領域
に this ポインタのアドレスを入れる機能
・ダイアログプロシージャで、ウィンドウの 32 ビット領域から
CDialog クラスの派生クラスのアドレスを取り出し、その
DialogProc() メンバ関数を呼び出す機能
「この3つの機能はどんなダイアログでも使える共通の機能なんです」
『確かに、 DialogBox() 関数呼ぶのはどのダイアログでも同じだし、
ディスパッチの仕組みもダイアログで変わることはないもんね』
「だから、 Version 18.05 ( No.392 ) の時の、 DialogProc() メンバ関数
の実装が CDialog クラスにあるバージョンだと、以下の問題があるわけで
す」
・様々な種類のダイアログを使用する場合、 CDialog クラスを複製する
ことになるが、そうすると共通機能が複製されてしまい、無駄になる。
・どのダイアログでも共通の機能と、それぞれのダイアログで特別な処理を
するダイアログプロシージャの機能を混ぜてしまうと、プログラムが
わかりにくくなり、また共通部分を間違って修正する可能性がある。
「つまり、複数のダイアログを使う場合、 CDialog クラスをコピペするこ
とになって、共通部分がどんどん増えていって困る、ってこと」
『なるほど、つまり今回みたいに分けると、』
・CDialog クラス
ダイアログの作成とディスパッチ機能を持つ。
・C111Dialog クラス
111ダイアログ用のダイアログプロシージャを持つ。
・C222Dialog クラス
222ダイアログ用のダイアログプロシージャを持つ。
・C333Dialog クラス
333ダイアログ用のダイアログプロシージャを持つ。
『って感じに、それぞれのダイアログ用のダイアログプロシージャだけどん
どん増やしていける、って感じになるんだ』
「そういうこと。継承を使うことで、共通部分の機能は基本クラスに入れて
おいて、個別の機能は派生クラスで、っていうことができるわけです」
『こうして分けちゃえば、無駄な部分が増えないし、プログラムが見やすく
なるからすっきりするわけね』
「こういうことができるのは、クラスの継承と、仮想関数のおかげ。この
仕組みを使うことで、処理を分けられるわけです」
『……なんだけど、実際にどう使われてるのかよく分からないかも』
「そうだね、ここはちょっと難しいかもしれないから、おさらいも兼ねて
仕組みを説明しておこうか」
『はーい!』
「まずはクラス図から。 CDialog クラスと CNewCalcDialog クラスの関係
をクラス図で書くと以下のようになります。クラス図の書き方については
Version 17.01 ( No.356 ) を参照してください」
┌────────────────────────┐
│CDialog │
├────────────────────────┤
│ m_pcDialog [static メンバ変数] │
├────────────────────────┤
│ DispatchDialogProc() [static メンバ関数] │
│ DoModal() │
│ DialogProc() [純粋仮想関数] │
└────────────────────────┘
△
│
│
┌────────────────────────┐
│CNewCalcDialog │
├────────────────────────┤
├────────────────────────┤
│ DialogProc() │
└────────────────────────┘
「 CDialog クラスが基本クラス、 CNewCalcDialog クラスが派生クラスな
ので、 CNewCalcDialog クラスから CDialog クラスへと三角矢印が引かれ
ています」
『こうしてみると機能が完全に分かれてるって分かるね。
CNewCalcDialog クラスの DialogProc() メンバ関数は、 CDialog クラスの
を実装してるんだよね』
「そう、 CDialog クラスの DialogProc() メンバ関数は純粋仮想関数だか
ら中身がありません。中身は CNewCalcDialog クラスで作ってるわけです」
『これが呼べるのは、 Version 17.16 ( No.371 ) の仕組みなんだよね』
「そう、仮想関数は【仮想関数テーブル】にアドレスが入れられていて、そ
のポインタを通して呼び出される仕組みになっています。 Main.cpp の
WinMain() 関数を見てみると、 CNewCalcDialog クラスの変数が作られてい
ます」
// CDialog クラスでダイアログを作成します。
CNewCalcDialog cDialog;
cDialog.DoModal( p_hInstance, IDD_MAIN );
「継承しているクラスの変数を作ると、派生クラスの中に基本クラスが作ら
れる形になって、さらに【仮想関数テーブル( vftable )】が作られてそ
の中に仮想関数のアドレスが保存されます」
┌CNewCalcDialog クラスの変数 cDialog ──────────┐
│ CNewCalcDialog クラスの部分 │
│┌─────────────────────────┐ │
││ CDialog クラスの部分 │ │
││ DialogProc() メンバ関数 │ │
││ __vfptr ↑ │ │
│└──↓─────↑────────────────┘ │
└───↓─────↑──────────────────┘
↓ ↑
vftable ↑
┌────────↑────────────┐
0│CNewCalcDialog::DialogProc()のアドレス │
└─────────────────────┘
「 DialogProc() メンバ関数がポインタを通して呼び出されると、仮想関数
なので仮想関数テーブルからアドレスを探し出して呼び出されます。これを
してるのが CDialog クラスの DispatchDialogProc() メンバ関数」
// ダイアログの 32 ビット整数に格納されている
// this ポインタを取りだします。
CDialog *pcDialog
= (CDialog *)GetWindowLong( p_hDlgWnd, GWL_USERDATA );
// 略
// メンバ関数のダイアログプロシージャを呼び出します。
return
pcDialog->DialogProc
( p_hDlgWnd
, p_uiMessage
, p_wParam
, p_lParam
);
『あ、そういえばポリモーフィズムってポインタを通さないとできないんだ
よね』
「 Version 17.13 ( No.368 ) で説明したように、 pcDialog ポインタには
実際には WinMain() 関数で作った CNewCalcDialog クラスの cDialog 変数
のアドレスが入ってます」
『そのアドレスが入ったポインタを使って DialogProc() メンバ関数を呼び
出すと、 CDialog クラスの DialogProc() メンバ関数は中身がないけど、
仮想関数テーブルを使って CNewCalcDialog クラスの方が呼び出されるから
大丈夫、ってわけね』
「そういうこと。ディスパッチの部分も含めるとこういう仕組みになります」
┌ウィンドウ─────────────────┐
│ ※イベント発生!※→→→→→→→→→→→→→→→
│┌32ビット領域─────────────┐ │ ↓(1)
││CNewCalcDialog クラスの変数、 │ │ ↓イベントが発生
││cDialog 変数のアドレス │ │ ↓して、ウィンドウ
│└───────────────────┘ │ ↓プロシージャが
└─────────────────↑────┘ ↓呼び出されます
↑ ↓
┌CDialog::DispatchDialogProc() ──↑───┐ ↓
│(2) GetWindowLong() で CDialog 派生クラス │ ↓
│ の変数のアドレスを取得します │ ←←
│(3) そのアドレスを使って CDialog クラスの │
│ DialogProc() メンバ関数を呼び出します │
└────────────────↓────┘
↓
┌CNewCalcDialog クラスの変数 cDialog ──────────┐
│ CNewCalcDialog クラスの部分 │
│┌─────────────────────────┐ │
││ CDialog クラスの部分 │ │
││ (4)DialogProc() メンバ関数は純粋仮想関数なので │ │
││ 仮想関数テーブルを見に行きます │ │
││ ↓ │ │
││ ↓ (6)DialogProc() メンバ関数で │ │
││ ↓ イベント処理をします │ │
│└──↓─────────────↑────────┘ │
└───↓─────────────↑──────────┘
↓ ↑
仮想関数テーブル vftable ↑
┌────────────────↑────┐
│(5)CNewCalcDialog クラスの ↑ │
│ DialogProc() メンバ関数の ↑ │
│ アドレスが書かれているので呼び出します │
└─────────────────────┘
『ふっ、複雑……』
「 Version 18.06 ( No.393 ) の図と合わせて、どの辺が変わったか確認し
てみてください」
/*
Preview Next Story!
*/
『図で見ても複雑なものは複雑!』
「確かにこの仕組みは複雑かも」
『 MFC ってこんなに複雑なことしてるのねー』
「ううん、 MFC はもっと複雑なことしてディスパッチしてるけど」
『えええっ!!』
「 MFC はサブクラス化とかメンバ関数ポインタとか色々使ってるから」
『というわけで次回』
< Version 18.09 イベントごとにメンバ関数を分ける >
「につづく!」
『? 次回ってその MFC の説明じゃないの?』
「……その説明はなくなりました」
『えええーっ!?』