#pragma twice

KAB-studio > プログラミング > #pragma twice > 368 Version 17.13 仮想関数とオーバーライド

#pragma twice 368 Version 17.13 仮想関数とオーバーライド

前のページへ 表紙・目次へ 次のページへ

 Version 17.13
仮想関数とオーバーライド

「前回はポリモーフィズムについて説明しました」
『ってゆーかわけわかんねー!』

    // 基本クラスのポインタにキャストします。
    CData *pcData = &cDerivedData;
    // このポインタを通して仮想関数を呼び出します。
    pcData->Output2();
    // CDerivedData::Output2()

『ってなってたら、絶対に CData クラスの Output2() メンバ関数が呼ばれ
るって思うって』
「でもそうはならないんです。元々のクラスが CDerivedData クラスだか
ら」

    // CDerivedData クラスの変数を作成します。
    CDerivedData cDerivedData;

「さらに、 Output2() メンバ関数は仮想関数で、 CDerivedData クラスで
オーバーライドされているから」

// 派生クラスの Output2() メンバ関数。
// オーバーライドしています。
void CDerivedData::Output2()
{
    OutputDebugString( "CDerivedData::Output2()\n" );
}

「そのため、図にすると以下のような関係になります」

CDerivedData: 0x00000001
┌───────┐       ┌───────┐
│ cDerivedData │       │ pcData       │
│ ┌────┐ │ ←←←│              │
│ │ cData  │ │       │ 0x00000001   │
│ │        │ │       └───────┘
│ │ __vfptr→→→→→
│ └────┘ │    ↓
└───────┘    ↓
                      ↓
       ←←←←←←←←
      ↓
      ↓
   vftable
  ┌──────────────────┐
 0│CData::Output1()のアドレス          │
  ├──────────────────┤
 1│CDerivedData::Output2()のアドレス   │
  └──────────────────┘

「 pcData ポインタを通して Output2() メンバ関数を呼び出す場合、この
メンバ関数は仮想関数なので、普通には呼び出さず、仮想関数テーブルを通
して呼び出そうとします」
『仮想関数だから?』
「 Version 17.10 ( No.365 ) と Version 17.11 ( No.366 ) で説明したよ
うに仮想関数テーブルは仮想関数がないと作られないからね」
『あ、そか』
「仮想関数を呼び出す際には、仮想関数テーブルから該当するメンバ関数の
アドレスを取得して、そのアドレスを関数ポインタとして呼び出します」
『すると、 pcData 、つまり CData クラスのポインタを通して呼び出して
いても、仮想関数テーブルにはオーバーライドした方のアドレスが書かれて
るから、そっちが呼び出されると』
「そういうこと。ポインタの参照先が CDerivedData クラスの変数だから、
仮想関数テーブルには CDerivedData クラスでオーバーライドした方の
アドレスが書かれています。だから呼び出されるわけ」
『? もしかして最初に作った変数の型が重要、ってこと?』
「そう!」

    // CDerivedData クラスの変数を作成します。
    CDerivedData cDerivedData;

「という形で変数を作った時点で、仮想関数テーブルが作られ、その中に
オーバーライドした方のアドレスが格納されます」
『つまりこの変数が CDerivedData クラスだから、 CDerivedData クラスで
オーバーライドした方の Output2() メンバ関数が呼び出されたわけねー』
「実際に試してみようか。元の変数が CData クラスの場合、 
Output2() メンバ関数を呼び出しても CData クラスの Output2() メンバ関数
が呼び出されます」

    // CData クラスの変数を作成します。
    CData cData;

    // 基本クラスのポインタにキャストします。
    CData *pcData = &cData;
    // このポインタを通して仮想関数を呼び出します。
    pcData->Output2();
    // CData::Output2()

『あ、ホントだ』
「図にするとこうなるからね」


CData: 0x00000001        ┌───────┐
 ┌────┐            │ pcData       │
 │ cData  │←←←←←←│              │
 │        │            │ 0x00000001   │
 │ __vfptr→→→→      └───────┘
 └────┘     ↓
                  ↓
       ←←←←←←
      ↓
      ↓
   vftable
  ┌──────────────────┐
 0│CData::Output1()のアドレス          │
  ├──────────────────┤
 1│CData::Output2()のアドレス          │
  └──────────────────┘

「 cData 変数は CData クラスの変数だから、仮想関数テーブルには 
CData クラスの仮想関数のアドレスが格納されます」
『 pcData から呼び出しても、仮想関数テーブルには CData クラスの
メンバ関数のアドレスが書かれてるからそっちが呼び出されるってわけね』
「図で書けば分かりやすいでしょ」
『というか、さっきのプログラムからこの図がイメージできるか、っていう
ことが問題よね』
「う、そうかも……」
『質問!』
「はい火美ちゃん」
『仮想関数じゃない場合ってどうなるの?』
「仮想関数じゃない場合はこうなりません。 pcData 変数の型を見て、その
メンバ関数が呼び出されます。たとえば以下のように virtual を取り除い
た場合」

// Data.h

// CData クラス。
class CData
{
public:
    void Output1();
    void Output2();
};

「これで以下のように実行すると CData クラスの Output2() メンバ関数が
呼び出されます」

    // CDerivedData クラスの変数を作成します。
    CDerivedData cDerivedData;

    // 基本クラスのポインタにキャストします。
    CData *pcData = &cDerivedData;
    // このポインタを通して仮想関数を呼び出します。
    pcData->Output2();
    // CData::Output2()

『うわホントだ!』
「この違いはコンパイルするときに決まります。コンパイラが、メンバ関数
を呼び出している箇所を見たときに」

・仮想関数じゃない→「->」の左側にあるポインタの型のメンバ関数を呼び出す
・仮想関数です    →仮想関数テーブルを通して呼び出す

「という形でメンバ関数の呼び出し方を決めるんです」
『仮想関数かそうじゃないかで呼び出し方が変わるんだ……』
「だから、ポリモーフィズムを機能させたくない場合には仮想関数にしなけ
ればいいんです」
『あ、それだけでいいんだ』
「仮想関数はちゃんと理解していないとバグの原因になるから、必要な時以
外は仮想関数にしない方がいいかな」
『かも……もひとつ質問!』
「はい火美ちゃん」
『わざわざポインタを使ってたけど、これってポインタ使わない場合はどう
なるの?』
「つまりこういうこと?」

    // CDerivedData クラスの変数を作成します。
    CDerivedData cDerivedData;

    // 基本クラスの変数にキャストします。
    CData cData = cDerivedData;
    // この変数を通して仮想関数を呼び出します。
    cData.Output2();
    // CData::Output2()

『あ、この時もオーバーライドした方は呼ばれないんだ』
「だってそうでしょ。 cDerivedData 変数と cData 変数は別なんだから」

CDerivedData: 0x00000001
┌───────┐
│ cDerivedData │
│ ┌────┐ │
│ │ cData  │ │
│ │        │ │
│ │ __vfptr→→→→→
│ └────┘ │    ↓
└───────┘    ↓
       ←←←←←←←←
      ↓
   vftable
  ┌──────────────────┐
 0│CData::Output1()のアドレス          │
  ├──────────────────┤
 1│CDerivedData::Output2()のアドレス   │
  └──────────────────┘

CData: 0x00000001 
 ┌────┐     
 │ cData  │
 │        │
 │ __vfptr→→→→ 
 └────┘     ↓
       ←←←←←←
      ↓
   vftable
  ┌──────────────────┐
 0│CData::Output1()のアドレス          │
  ├──────────────────┤
 1│CData::Output2()のアドレス          │
  └──────────────────┘

「という感じに、それぞれ独立した変数だから」
『そか、 cData 変数の仮想関数テーブルは、 CData クラスなんだから
CDerivedData クラスの Output2() のアドレスがあるわけないんだね』
「そういうこと。だから、ポリモーフィズムが機能するのはポインタを使っ
た時ってことだね」

/*
    Preview Next Story!
*/
『おー、なんとなく分かってきたかも』
「図を見れば分かるかな」
『あと何度も試してるしね、でも』
「でも?」
『何に使えるの、これ』
「というわけで次回」
< Version 17.14 多態性、多相性 >
『につづく!』
「まぁはっきり言うと C++ ではあまり使わないんだけどね」
『え”』
 
del.icio.us 登録する
Yahoo!ブックマーク 詳細を表示 users
livedoorクリップ 詳細を表示 livedoorクリップ ブックマーク数
はてなブックマーク 詳細を表示 はてなブックマーク ブックマーク数
RSSに登録
del.icio.us 登録する
Yahoo!ブックマーク 詳細を表示 users
livedoorクリップ 詳細を表示 livedoorクリップ ブックマーク数
はてなブックマーク 詳細を表示 はてなブックマーク ブックマーク数
 
このページは、Visual C++ 6.0を用いた C++ 言語プログラミングの解説を行う#pragma twiceの一コンテンツです。
詳しい説明は#pragma twiceのトップページをご覧ください。