仮想関数???

こんにちはぷないしんです。

 

今日は仮想関数

バーチャルな関数・・・のお話です。

 

前置きとして

せっかくなので以前使った”会員”のクラスに【シルバーメンバー】クラスを追加していこうと思います。

 

一般会員クラスとの差異は

・シニア特典でレベルによって内容の違うint形のデータメンバsenior_level

・上記のメンバの度合いを取得、設定するゲッターとセッターget_senior_level、set_care_level

 

それでは早速ヘッダとソースです。

 

silver.h

#ifndef ___SilverMember
#define ___SilverMember

#include <string>
#include "Member.h"

//シルバー会員クラス
class SilverMember : public Member{
    int senior_level;

public:
    //コンストラクタ
    SilverMember(const std::string& nameint nodouble wint level = 0);
    //senior_levelのゲッター
    int get_senior_level() const { return senior_level; }
    //senior_levelのセッター
    void set_senior_level (int level){
        senior_level = (level >= 1 && level <= 5) ? level :0;
    }

    void print() const;
};

#endif

 SilveMember.cpp

#include <string>
#include <iostream>
#include "silver.h"

using namespace std;


//コンストラクタ
SilverMember::SilverMember(const stringnameint nodouble wint level)
                            :Member(namenow)
{
    set_senior_level(level);
}

void SilverMember::print() const
{
    cout << "No," << no() << ":" << name() << "(" << get_weight() << "kg)"
         << "Level =" << senior_level << "\n";
}

 セッターの動きによって、Levelの値は必ず0以上、5以下になります。

また、メンバ関数printは、そのレベルを表示します。

下の様にクラスによって表示項目が違います。

 

・No.1 Kimura Takuma is 46.5kg    

↑一般クラス:会員番号、氏名、体重

・No.3 Kusanagi Takeshi is 58.5kg Special is Ippon Satisfaction 

↑VIPクラス:会員番号、氏名、体重、特典

・No.2 Nakai Masamune is 59.7kg Level = 3

↑シルバークラス:会員番号、氏名、体重、レベル

 

VIPクラスと同じように、シルバークラスは一般クラスの派生なので構成としてはこんな感じですね。

f:id:punainen:20210407140247p:plain

Aクラス階層図

Memberからpublic派生しているシルバー会員クラスは”一般クラスの一種”となります。

(is-Aの関係が成立するということです)

メンバ関数のいんぺい

基底クラスのMemberにメンバ関数printが存在し、派生クラスであるVIPMember、SilverMemberにもこの関数があります。

この様に、基底クラスのメンバ関数と同じ名前のメンバ関数が派生先のクラスで定義されると、派生クラスのメンバ関数は基底クラスのメンバ関数隠します。

そのことを次のソースで確認してみましょう。

内容はVIPMember型、SilverMember型の各オブジェクトに対して、メンバ関数printを呼び出すだけの簡単なものです。

 

MemberPrint.cpp

#include <iostream>
#include "VIPMember0.h"
#include "silver.h"

using namespace std;


int main(){
    VIPMember0 Kusanagi("Kusanagi Takeshi"358.5,"Ippon satisfaction bar");
    SilverMember Nakai("Nakai Masamune"259.73);

    Kusanagi.print();
    Nakai.print();
}

f:id:punainen:20210407151026p:plain

実行結果

kusanagiに対してはVIPMember::printが呼び出され、NakaiはSilverMember::printが呼び出されています。

この例では関数printの仮引数の型と個数は同一です。とまれ、関数名さえ同じなら仮引数の型や戸数が違っていても隠ぺいは行われます。

基底クラスのメンバ関数と同一名の派生クラスのメンバ関数は、仮引数の型や数が違っていても、基底クラスのメンバ関数を隠ぺいする。

 

VIPMemberとSilverMemberは、is-Aの関係であるpublic派生でMemberクラスから派生しています。

なので、Memberから継承したメンバ関数printは、VIPMemberとSilverMemberでも公開メンバとして存在します。

 

継承したメンバ関数は、名前が隠されているだけで会って、存在が消える訳ではありません。

実際に隠ぺいされている基底クラスのメンバ関数は、クラスの外部から呼び出せます。

 

 

それを確認してみましょう。

Memberprint2.cpp

#include <iostream>
#include "VIPMember.h"
#include "silver.h"

using namespace std;


int main(){
    VIPMember Kusanagi("Kusanagi Takeshi"358.5,"Ippon satisfaction bar");
    SilverMember Nakai("Nakai Masamune"259.73);

    Kusanagi.Member::print();
    Nakai.Member::print();
}

 

f:id:punainen:20210407155903p:plain

実行結果

有効範囲解決演算子「::」を使い、Member::print()により、基底クラスMemberのメンバ関数printを呼び出しています。

 

基底クラスから継承したメンバと同名のメンバが派生クラス内にある時、”基底クラス名::メンバ名”で基底クラスから継承した(隠ぺいされている)メンバにアクセスできる。

静的な型

次は下のコードです。

関数put_memberは、引数mに受け取った会員の情報を表示します。

その時、体重が55kg以上ならば先頭に@を付けます。

Memberprintref.cpp

#include <iostream>
#include "Member.h"
#include "VIPMember.h"
#include "silver.h"

using namespace std;

void put_member(const Member& m)
{
    cout << (m.get_weight() >= 55 ? "@" : " ");
    m.print();
}

int main(){
    Member Kimura("Kimura Takuma"148);
    VIPMember Kusanagi("Kusanagi Takeshi"358.5,"Ippon satisfaction bar");
    SilverMember Nakai("Nakai Masamune"259.73);

    put_member(Kimura);
    put_member(Kusanagi);
    put_member(Nakai);
}

 

 

f:id:punainen:20210407161030p:plain

実行結果

 関数put_memberが受け取るmの型は(constな)Member&型です。

main関数から三回呼び出している実引数は

・Member型kimura

・VIPMember型kusanagi

・SilverMember型Nakai

への参照です。

kusanagiへの実行結果から

①呼び出された関数put_memberの仮引数mがVIPMember型であるkusanagiを参照すること

②put_member宣言時でmに対して呼び出されるprintがVIPMember::printではなく、Member::printになっていること(特典が表示されていない)

が分かります。

①については、

基底クラスへのポインタ/参照は、派生クラスのオブジェクトを指す/参照できるので不思議はありません。

②になる理由は単純でmの型が【Memberへの参照型】だからです。

基底クラスへのポインタ/参照が指す/参照する式を実行して得られるのは、派生クラスの型ではなく、基底クラス型です。

 

つまり

オブジェクトmの静的な型はMemberである

ということです。

 

静的な型の表示方法はtypeidを使えば出せるので確認してみましょう。

MemberStaticType.cpp

#include <iostream>
#include <typeinfo>
#include "Member.h"
#include "VIPMember.h"
#include "silver.h"

using namespace std;

int main(){
    VIPMember Kusanagi("Kusanagi Takeshi"358.5,"Ippon satisfaction bar");
    
    Memberptr = &Kusanagi;
    Memberref =  Kusanagi;
    
    cout << typeid(*ptr).name() << "\n";
    cout << typeid(ref).name() << "\n";

}

 

f:id:punainen:20210407162803p:plain

実行結果

 はい、ということでどちらもVIPMember型ではなく、Member型ですね。

 

仮想関数

ということで本題の仮想関数の話に入っていきます。

先ほどのままではさまざまな制約を受けます

 この問題を解決する手段の一つして仮想関数を使います

では一般会員クラスのヘッダ部分を改良していきましょう。

 

Member.h

//パンピークラス

#ifndef ___Member
#define ___Member

#include <string>

class Member{
    std::string full_name;
    int     number;
    double  weight;

public:
    //コンストラクタ
    Member(const std::string& nameint nodouble w);
    //名前
    std::string name()const{
        return full_name;
    }
    //番号
    int no() const {
        return number;
    }
    //体重返却
    double get_weight() const {
        return weight;
    }
    //体重設定
    void set_weight(double w){
        weight = (w > 0) ? w : 0;
    }
    //情報表示
    virtual void print() const;
};
#endif

 改良するといいながら前のヘッダーと違う所はprintの前にvirtualを追加したことだけです。

 

これでMemberPrintrfをコンパイル、実行してみましょう。

f:id:punainen:20210407165554p:plain

実行結果

 とまあこんな感じで特典情報やレベルが表示されました。

 

今までは呼び出される関数は、プログラムをコンパイルするときに静的に決定されていましたが、こうすることにより

mの参照先オブジェクトのクラスに所属するメンバ関数printの呼び出しで

基底クラスであるMember形への参照mを通じて、Memberのメンバ関数が呼び出されたり、派生クラスであるVipMemberやSeniorMemberのメンバ関数が呼び出されています。

なので、当然mの参照先のオブジェクトがどのクラスであるのかはコンパイル時には決定できません。

そのため、どのクラスのメンバ関数printを呼び出すのかはコンパイル時ではなく、プログラムを実行したとき、動的に決定されます。