「C++言語」の仮想関数について理解しよう!

今回は、「仮想関数」の仕組み等を学んでいきたいと思いますが、ここからは未学習の「未知の領域」です。

果たして自分に理解ができるのかできないのか・・・

やってみないことには何も学べないので、とにかく学習を進めていきたいと思います。

「基底クラス」のメンバ関数を利用する

「派生クラス」から「基底クラス」の関数を利用する方法を学んでいきたいと思います。

今回作成するクラスは「親子関係を持つ2つのクラス」で、

クラス継承関係

「SampleBase」というクラスを「基底クラス」とし、「SampleChild」クラスを「派生クラス」として作成してみたいと思います。

まず、「SampleBase」クラスを作成していきたいと思いますが、このクラスは、「greet」というメンバ関数を持っていて、「自分のクラス名を名乗って挨拶をする」という動作を行います。

「SampleBase」クラスの「ヘッダーファイル」は、

#ifndef ___CLASS_SampleBase
#define ___CLASS_SampleBase

class SampleBase {
public:
    void greet();
};

#endif

のようになり、「実装ファイル」は、

#include <iostream>
#include "SampleBase.h"

using namespace std;

void SampleBase::greet(){
    cout << "こんにちは「SampleBase」クラスです。" << endl;
}

のように作成しました。

この「SampleBase」クラスを継承しているのが「SampleChild」クラスなのですが、このクラスの「ヘッダーファイル」は、

#ifndef ___CLASS_SampleChild
#define ___CLASS_SampleChild

#include "SampleBase.h"

class SampleChild : public SampleBase {
public:
    void greet();
};

#endif

のように作成しました。このクラスの「実装ファイル」は、

#include <iostream>
#include "SampleChild.h"

using namespace std;

void SampleChild::greet(){
    cout << "こんにちは「SampleChildクラス」です。" << endl;
}

のようになります。

この「SampleChild」クラスから「基底クラス」の「SampleBase」クラスのメンバ関数を利用する場合は、

派生クラス型の変数.基底クラス名::基底クラスのメンバ関数名();

のようにプログラムを書くと、「基底クラス」の関数を実行することができます。

「main」関数を作ってみると、

#include <iostream>
#include "SampleBase.h"
#include "SampleChild.h"

using namespace std;

int main() {
	// 「親クラス」のオブジェクトを作成
    SampleBase sb;

    // 「親クラス」の「greet」関数を実行
    sb.greet();

    // 「子クラス」のオブジェクトを作成
    SampleChild sc;

    // 「子クラス」の「greet」関数を実行
    sc.greet();

    // 「子クラス」から「親クラス」の「greet」関数を実行
    sc.SampleBase::greet();

    return 0;
}

のようになります。

今回は3パターンの関数を実行していますが、最後のパターンで「派生クラス」から「基底クラス」のメンバ関数を呼び出しています。

このプログラムを実行すると、

こんにちは「SampleBaseクラス」です。
こんにちは「SampleChildクラス」です。
こんにちは「SampleBaseクラス」です。

最後の実行結果は、「基底クラス」の「greet」関数の実行結果であることが確認できますね。

「仮想関数」の仕組み

「仮想関数」を理解するために、今回は新たに「DisplayData」クラスを作り、「display」関数を利用して、オブジェクトのメンバ関数を実行していきたいと思います。

「DisplayData」クラスの「ヘッダーファイル」は、

#ifndef ___CLASS_DisplayData
#define ___CLASS_DisplayData

#include "SampleBase.h"

class DisplayData {
public:
    void display(SampleBase& sb);
};

#endif

のようになり、「実装ファイル」の内容は、

#include <iostream>
#include "DisplayData.h"
#include "SampleBase.h"

using namespace std;

void DisplayData::display(SampleBase& sb){
    sb.greet();
}

のように作成しました。

「DisplayDataクラス」の「display」関数では、「SampleBase」クラスの参照を受け取り、「display」」関数を利用しています。

変数名の後に「&」を付けると、「参照」を受け取ることができるのですが、これは「ポインタ」では無いとのことで、若干混乱してきています(^^;)

「ポインタ」は変数などの「メモリアドレス」ですが、「参照」はただ、「そのデータを指すだけ」とのことで、何で似たような機能が2つもあるんだろうかと・・・

と愚痴っていても仕方が無いので、次に進んでいきたいと思います。

「main」関数は、

#include <iostream>
#include "SampleBase.h"
#include "SampleChild.h"
#include "DisplayData.h"

using namespace std;

int main() {

    // 「データ表示用クラス」のオブジェクト生成
    DisplayData dd;

    // 「親クラス」のオブジェクトを作成
    SampleBase sb;

    // 「子クラス」のオブジェクトを作成
    SampleChild sc;

    //「親クラス」のデータを表示
    dd.display(sb);

    //「子クラス」のデータを表示
    dd.display(sc);

    return 0;
}

のように作成していますが、「SampleBase」クラスと「SampleChild」クラスは以前に作ったものをそのまま利用しています。

このプログラムを実行してみると、

こんにちは「SampleBaseクラス」です。
こんにちは「SampleBaseクラス」です。

と表示されます。

しかし、2つ目の出力は「SampleChild」クラスのオブジェクトの出力なのに、なぜか「SampleBase」クラスの「greet」関数が実行されています。

このような実行結果になってしまうのは、「display」関数の引数が「SampleBase」クラス型のオブジェクトの参照となっているためで、受け取ったオブジェクトが「SampleChild」クラスのオブジェクトであっても、「SampleBase」クラスの「greet」関数が実行されてしまいます。

実際のオブジェクトの内容より「受け取った時のデータ型」を優先させた動作になっています。

では、実際に渡したオブジェクトの関数を実行するにはどうすればいいのかというと、

「SampleBase」クラスの「greet」関数に「virtual」というキーワードを追加することで、実行することができます。

「SampleBase」クラスの「ヘッダーファイル」を、

#ifndef ___CLASS_SampleBase
#define ___CLASS_SampleBase

class SampleBase {
public:
    virtual void greet();
};

#endif

のように変更します。

このプログラムを実行すると、

こんにちは「SampleBaseクラス」です。
こんにちは「SampleChildクラス」です。

のように表示され、「SampleChildクラス」の「greet」関数が実行されているのがわかりますね。

なかなか複雑に感じられる仕組みですが(自分にとっては・・・)もし、こういう動作になった時に何が原因なのかが理解できるようになりました。

→(前へ)「C++言語」の「継承」の仕組みを身に付けよう!

→(次へ)「例外処理」と「const」の仕組みについて理解しよう!

「C++言語」学習書籍

HOMEへ