C++復習五-多態と虚関数

4894 ワード

一、概説
C++は、基本クラスポインタを使用して、直接派生および間接派生を含むすべての派生クラスのメンバー変数とメンバー関数、特にメンバー関数に全方位的にアクセスできるようにするためのマルチステートを提供します.マルチステートがない場合は、メンバー変数にのみアクセスできます.
しかし、オブジェクトのメモリモデルは非常にきれいで、メンバー関数の情報は含まれていません.コンパイラは何に基づいてメンバー関数を見つけたのでしょうか.「C++虚関数表精講チュートリアル、ストレートスタンプ多態の実現メカニズム」の項で答えを示します.
虚関数があり、ベースクラスポインタがベースクラスオブジェクトを指すときにベースクラスのメンバーを使用する(メンバー関数とメンバー変数を含む)は、派生クラスオブジェクトを指すときに派生クラスのメンバーを使用します.言い換えれば、ベースクラスポインタはベースクラスに従って仕事をすることもできるし、派生クラスに従って仕事をすることもできます.それは多様な形態を持っているか、あるいは多様な表現があるので、この現象をマルチステート(Polymorphism)と呼びます.
二、多態の用途
上記の例を通して読者はまだ多態の用途を発見していないかもしれないが、確かに多態は小さなプロジェクトで活躍する場所が少ない.次の例では、あなたが軍事ゲームをしていて、敵が突然地上戦争を起こしたと仮定し、陸軍、空軍、およびすべての現役装備に作戦状態に入るように命令します.具体的なコードは以下の通りです.
#include 
using namespace std;

//  
class Troops{
public:
    virtual void fight(){ cout<fight();
    //  
    p = new Army;
    p ->fight();
    p = new _99A;
    p -> fight();
    p = new WZ_10;
    p -> fight();
    p = new CJ_10;
    p -> fight();
    //  
    p = new AirForce;
    p -> fight();
    p = new J_20;
    p -> fight();
    p = new CH_5;
    p -> fight();
    p = new H_6K;
    p -> fight();

    return 0;
}

実行結果:Strike back!--Army is fighting! ----99A(Tank) is fighting! ----WZ-10(Helicopter) is fighting! ----CJ-10(Missile) is fighting! --AirForce is fighting! ----J-20(Fighter Plane) is fighting! ----CH-5(UAV) is fighting! ----H-6K(Bomber) is fighting! この例では派生クラスが多く、マルチステートを使用しない場合は、複数のポインタ変数を定義する必要があり、混乱を招きやすい.一方,マルチステートでは,ポインタ変数pが1つですべての派生クラスの虚関数を呼び出すことができる.
この例からも,複雑な継承関係を持つ大中型プログラムに対して,多態はその柔軟性を増大させ,コードをより表現力を持たせることができることが分かった.
三、多態を構成する条件
「アカデミー派」の観点から言えば、パッケージング、継承、マルチステートはオブジェクト向けの3つの特徴であり、パッケージング、継承はそれぞれ「C++クラスメンバーのアクセス権およびクラスのパッケージング」「C++継承と派生簡明チュートリアル」で説明されているが、マルチステートとは、ベースクラスのポインタでベースクラスのメンバーにも、派生クラスのメンバーにもアクセスできることを意味する.以下は、マルチステートを構成する条件です.
  • は継承関係が存在しなければならない.
  • 継承関係には、同名の虚関数が必要であり、上書き関係である(関数のプロトタイプは同じ).
  • ベースクラスのポインタが存在し、このポインタによって虚関数が呼び出される.

  • 次の例では、さまざまな混乱状況について説明します.
    #include 
    using namespace std;
    
    //  Base
    class Base{
    public:
        virtual void func();
        virtual void func(int);
    };
    void Base::func(){
        cout< func();  //  void Derived::func()
        p -> func(10);  //  void Base::func(int)
        p -> func("http://c.biancheng.net");  //compile error
    
        return 0;
    }

    ベースクラスBaseではvoid func()を虚関数として宣言し、派生クラスDerivedのvoid func()が自動的に虚関数になります.pはベースクラスBaseのポインタであるが,派生クラスDerivedのオブジェクトを指す.文p -> func();は派生クラスの虚関数を呼び出し,多態を構成する.文p -> func(10);は、派生クラスに関数が上書きされていないため、ベースクラスの虚関数を呼び出す.文p -> func("http://c.biancheng.net");は、ベースクラスのポインタによってベースクラスから継承された過去のメンバーのみにアクセスでき、派生クラスの新しいメンバーにアクセスできないため、コンパイルエラーが発生しました.
    五、いつ虚関数を宣言するか
    まず、メンバー関数が存在するクラスがベースクラスになるかどうかを見ます.次に、クラスの継承後にメンバー関数が機能を変更する可能性があるかどうかを確認します.機能を変更する場合は、一般的に虚関数として宣言する必要があります.メンバー関数がクラスに継承された後に機能を変更する必要がなく、派生クラスがその関数を使用できない場合は、虚関数として宣言しないでください.
    六、C++虚構造関数の必要性
    コンストラクション関数は、派生クラスがベースクラスのコンストラクション関数を継承できないため、虚関数ではありません.
    もう1つの理由は、C++のコンストラクション関数は、オブジェクトの作成時に初期化作業を行うために使用され、コンストラクション関数を実行する前にオブジェクトが作成されておらず、ダミー関数テーブルは存在せず、ダミー関数テーブルへのポインタもないため、この時点でダミー関数テーブルを問い合わせることができず、どのコンストラクション関数を呼び出すか分からないためです.次のセクションでは、虚関数テーブルの概念について説明します.
    構造関数は、オブジェクトを破棄するときにクリーンアップ作業を行うために使用され、虚関数として宣言できます.また、虚関数として宣言する必要がある場合があります.
    #include 
    using namespace std;
    
    //  
    class Base{
    public:
        Base();
        ~Base();
    protected:
        char *str;
    };
    Base::Base(){
        str = new char[100];
        cout<

    行結果:Base constructor Derived constructor Base destructor----------Base constructor Derived constructor Derived destructor Base destructorこの例では、2つのクラスが定義されています.ベースクラスBaseと派生クラスDerivedは、それぞれ独自のコンストラクション関数と構造関数を持っています.コンストラクション関数では、100 charタイプのメモリ領域が割り当てられます.構造関数では、これらのメモリが解放されます.pb、pdはそれぞれベースクラスポインタと派生クラスポインタであり、いずれも派生クラスオブジェクトを指し、最後にdeleteを使用してpb、pdが指すオブジェクトを破棄する.実行結果から、文delete pb;はベースクラスの構造関数のみを呼び出し、派生クラスの構造関数を呼び出していないことがわかる.一方、文delete pd;は、派生クラスとベースクラスの構造関数を同時に呼び出す.この例では、派生クラスの構造関数を呼び出さないと、nameが指す100 charタイプのメモリ領域が解放されません.プログラムの実行が完了してOSによって回収されない限り、これらのメモリを解放する機会はありません.これは典型的なメモリ漏洩です.1)なぜdelete pb;は派生クラスの構造関数を呼び出さないのでしょうか.ここの解析関数は非虚関数であるため、ポインタによって非虚関数にアクセスすると、コンパイラはポインタのタイプに基づいて呼び出す関数を決定します.すなわち,ポインタがどのクラスを指すかは,どのクラスの関数を呼び出すことを前の章で何度も強調した.pbはベースクラスのポインタであるため、ベースクラスのオブジェクトを指すか派生クラスのオブジェクトを指すかにかかわらず、常にベースクラスを呼び出す構造関数である.2)なぜdelete pd;が派生クラスとベースクラスの構造関数を同時に呼び出すのか.pdは派生クラスのポインタであり、コンパイラはそのタイプに応じて派生クラスの構造関数に一致し、派生クラスの構造関数を実行する過程で、ベースクラスの構造関数を呼び出す.派生クラスの構造関数は常にベースクラスの構造関数を呼び出し、このプロセスは暗黙的に完了します.これは「C++構造関数」の項で説明しています.上のコードを変更し、ベースクラスの構造関数を虚関数として宣言します.
    class Base{
    public:
        Base();
        virtual ~Base();
    protected:
        char *str;
    };