runtime polymorphism c
C ++でのランタイムポリモーフィズムの詳細な研究。
ランタイムポリモーフィズムは、動的ポリモーフィズムまたは遅延バインディングとも呼ばれます。実行時ポリモーフィズムでは、関数呼び出しは実行時に解決されます。
対照的に、コンパイル時または静的ポリモーフィズムでは、コンパイラーは実行時にオブジェクトを推測し、オブジェクトにバインドする関数呼び出しを決定します。 C ++では、ランタイムポリモーフィズムはメソッドのオーバーライドを使用して実装されます。
このチュートリアルでは、ランタイムポリモーフィズムについて詳しく説明します。
=> ここですべてのC ++チュートリアルを確認してください。
学習内容:
関数のオーバーライド
関数のオーバーライドは、基本クラスで定義された関数が派生クラスで再度定義されるメカニズムです。この場合、関数は派生クラスでオーバーライドされると言います。
関数のオーバーライドはクラス内では実行できないことを覚えておく必要があります。関数は、派生クラスでのみオーバーライドされます。したがって、関数のオーバーライドには継承が存在する必要があります。
2つ目は、オーバーライドする基本クラスの関数は、同じシグネチャまたはプロトタイプを持つ必要があります。つまり、同じ名前、同じ戻り値の型、同じ引数リストを持つ必要があります。
メソッドのオーバーライドを示す例を見てみましょう。
#include using namespace std; class Base { public: void show_val() { cout << 'Class::Base'< 出力:
クラス::ベース
クラス::派生
上記のプログラムには、基本クラスと派生クラスがあります。基本クラスには、派生クラスでオーバーライドされる関数show_valがあります。 main関数では、BaseクラスとDerivedクラスのそれぞれにオブジェクトを作成し、各オブジェクトでshow_val関数を呼び出します。目的の出力を生成します。
各クラスのオブジェクトを使用した関数の上記のバインディングは、静的バインディングの例です。
ここで、基本クラスポインターを使用し、そのコンテンツとして派生クラスオブジェクトを割り当てるとどうなるかを見てみましょう。
サンプルプログラムを以下に示します。
#include using namespace std; class Base { public: void show_val() { cout << 'Class::Base'; } }; class Derived:public Base { public: void show_val() //overridden function { cout <<'Class::Derived'; } }; int main() { Base* b; //Base class pointer Derived d; //Derived class object b = &d; b->show_val(); //Early Binding }
出力:
クラス::ベース
これで、出力が「Class :: Base」であることがわかります。したがって、ベースポインタが保持しているタイプオブジェクトに関係なく、プログラムは、ベースポインタがのタイプであるクラスの関数の内容を出力します。この場合、静的リンクも実行されます。
ベースポインタを出力し、内容を修正し、適切にリンクするために、関数の動的バインディングを行います。これは、次のセクションで説明する仮想関数メカニズムを使用して実現されます。
仮想機能
オーバーライドされた関数は関数本体に動的にバインドする必要があるため、「virtual」キーワードを使用して基本クラスの関数を仮想化します。この仮想関数は、派生クラスでオーバーライドされる関数であり、コンパイラーはこの関数の遅延バインディングまたは動的バインディングを実行します。
次に、上記のプログラムを変更して、次のように仮想キーワードを含めます。
#include using namespace std;. class Base { public: virtual void show_val() { cout << 'Class::Base'; } }; class Derived:public Base { public: void show_val() { cout <<'Class::Derived'; } }; int main() { Base* b; //Base class pointer Derived d; //Derived class object b = &d; b->show_val(); //late Binding }
出力:
クラス::派生
したがって、上記のBaseのクラス定義では、show_val関数を「仮想」として作成しました。基本クラス関数が仮想化されているため、派生クラスオブジェクトを基本クラスポインターに割り当ててshow_val関数を呼び出すと、実行時にバインディングが発生します。
したがって、基本クラスポインタには派生クラスオブジェクトが含まれているため、派生クラスのshow_val関数本体は関数show_valにバインドされ、したがって出力にバインドされます。
C ++では、派生クラスのオーバーライドされた関数をプライベートにすることもできます。コンパイラは、コンパイル時にオブジェクトのタイプをチェックし、実行時に関数をバインドするだけなので、関数がパブリックまたはプライベートであっても違いはありません。
関数が基本クラスで仮想として宣言されている場合、その関数はすべての派生クラスで仮想になることに注意してください。
ただし、これまで、バインドする正しい関数を識別する際に仮想関数がどのように正確に役割を果たすか、つまり、バインディングが実際にどのように発生するかについては説明していません。
仮想関数は、の概念を使用して、実行時に関数本体に正確にバインドされます。 仮想テーブル(VTABLE) と呼ばれる隠しポインタ _vptr。
これらの概念は両方とも内部実装であり、プログラムで直接使用することはできません。
仮想テーブルと_vptrの動作
まず、仮想テーブル(VTABLE)とは何かを理解しましょう。
コンパイラは、コンパイル時に、仮想関数を持つクラスと、仮想関数を持つクラスから派生したクラスに対して、それぞれ1つのVTABLEを設定します。
VTABLEには、クラスのオブジェクトから呼び出すことができる仮想関数への関数ポインターであるエントリーが含まれています。仮想関数ごとに1つの関数ポインタエントリがあります。
純粋仮想関数の場合、このエントリはNULLです。 (これが、抽象クラスをインスタンス化できない理由です)。
次のエンティティである_vptrは、vtableポインターと呼ばれ、コンパイラーが基本クラスに追加する非表示のポインターです。この_vptrは、クラスのvtableを指します。この基本クラスから派生したすべてのクラスは、_vptrを継承します。
c ++二分木の例
仮想関数を含むクラスのすべてのオブジェクトは、この_vptrを内部的に格納し、ユーザーに対して透過的です。オブジェクトを使用した仮想関数へのすべての呼び出しは、この_vptrを使用して解決されます。
例を挙げて、vtableと_vtrの動作を示しましょう。
#include using namespace std; class Base_virtual { public: virtual void function1_virtual() {cout<<'Base :: function1_virtual()
';}; virtual void function2_virtual() {cout<<'Base :: function2_virtual()
';}; virtual ~Base_virtual(){}; }; class Derived1_virtual: public Base_virtual { public: ~Derived1_virtual(){}; virtual void function1_virtual() { coutfunction2_virtual(); delete (b); return (0); }
出力:
Derived1_virtual :: function1_virtual()
ベース:: function2_virtual()
上記のプログラムには、2つの仮想関数と1つの仮想デストラクタを持つ基本クラスがあります。また、基本クラスからクラスを派生させました。 1つの仮想関数のみをオーバーライドしました。 main関数では、派生クラスポインタがベースポインタに割り当てられます。
次に、基本クラスのポインターを使用して両方の仮想関数を呼び出します。オーバーライドされた関数は、基本関数ではなく、呼び出されたときに呼び出されることがわかります。一方、2番目のケースでは、関数がオーバーライドされないため、基本クラスの関数が呼び出されます。
ここで、vtableと_vptrを使用して上記のプログラムが内部的にどのように表されるかを見てみましょう。
前の説明のように、仮想関数を持つ2つのクラスがあるため、各クラスに1つずつ、合計2つのvtableがあります。また、_vptrは基本クラスに存在します。
上に示したのは、上記のプログラムのvtableレイアウトがどのようになるかを図で表したものです。基本クラスのvtableは単純です。派生クラスの場合、function1_virtualのみがオーバーライドされます。
したがって、派生クラスvtableでは、function1_virtualの関数ポインターが派生クラスのオーバーライドされた関数を指していることがわかります。一方、function2_virtualの関数ポインタは、基本クラスの関数を指します。
したがって、上記のプログラムでは、ベースポインタに派生クラスオブジェクトが割り当てられている場合、ベースポインタは派生クラスの_vptrを指します。
したがって、b-> function1_virtual()の呼び出しが行われると、派生クラスからのfunction1_virtualが呼び出され、関数の呼び出しb-> function2_virtual()が行われると、この関数ポインターが基本クラス関数を指すため、基本クラス関数と呼ばれます。
純粋仮想関数と抽象クラス
前のセクションで、C ++の仮想関数の詳細を見てきました。 C ++では、「 純粋仮想関数 」は通常ゼロに相当します。
純粋仮想関数は、次のように宣言されます。
virtual return_type function_name(arg list) = 0;
「」と呼ばれる純粋仮想関数を少なくとも1つ持つクラス 抽象クラス 」。抽象クラスをインスタンス化することはできません。つまり、抽象クラスのオブジェクトを作成することはできません。
これは、VTABLE(仮想テーブル)内のすべての仮想関数に対してエントリが作成されることがわかっているためです。ただし、純粋仮想関数の場合、このエントリにはアドレスがないため、不完全になります。そのため、コンパイラは、不完全なVTABLEエントリを持つクラスのオブジェクトを作成することを許可しません。
これが、抽象クラスをインスタンス化できない理由です。
以下の例は、純粋仮想関数と抽象クラスを示しています。
#include using namespace std; class Base_abstract { public: virtual void print() = 0; // Pure Virtual Function }; class Derived_class:public Base_abstract { public: void print() { cout <<'Overriding pure virtual function in derived class
'; } }; int main() { // Base obj; //Compile Time Error Base_abstract *b; Derived_class d; b = &d; b->print(); }
出力:
派生クラスの純粋仮想関数をオーバーライドする
上記のプログラムには、抽象クラスにする純粋仮想関数を含むBase_abstractとして定義されたクラスがあります。次に、Base_abstractからクラス「Derived_class」を派生させ、その中の純粋仮想関数の出力をオーバーライドします。
main関数では、その最初の行はコメント化されていません。これは、コメントを外すと、抽象クラスのオブジェクトを作成できないため、コンパイラーがエラーを出すためです。
ただし、2行目以降のコードは機能します。基本クラスポインタを正常に作成し、それに派生クラスオブジェクトを割り当てることができます。次に、派生クラスでオーバーライドされたprint関数の内容を出力するprint関数を呼び出します。
抽象クラスのいくつかの特徴を簡単にリストしましょう。
- 抽象クラスをインスタンス化することはできません。
- 抽象クラスには、少なくとも1つの純粋仮想関数が含まれています。
- 抽象クラスをインスタンス化することはできませんが、このクラスへのポインターまたは参照をいつでも作成できます。
- 抽象クラスは、純粋仮想関数とともに、プロパティやメソッドなどの実装を持つことができます。
- 抽象クラスからクラスを派生させる場合、派生クラスは抽象クラス内のすべての純粋仮想関数をオーバーライドする必要があります。そうしなかった場合、派生クラスも抽象クラスになります。
仮想デストラクタ
クラスのデストラクタは仮想として宣言できます。アップキャストを行うとき、つまり派生クラスオブジェクトを基本クラスポインタに割り当てるときはいつでも、通常のデストラクタは許容できない結果を生成する可能性があります。
例えば、次の通常のデストラクタのアップキャストを検討してください。
#include using namespace std; class Base { public: ~Base() { cout << 'Base Class:: Destructor
'; } }; class Derived:public Base { public: ~Derived() { cout<< 'Derived class:: Destructor
'; } }; int main() { Base* b = new Derived; // Upcasting delete b; }
出力:
基本クラス::デストラクタ
上記のプログラムでは、基本クラスから継承された派生クラスがあります。主に、派生クラスのオブジェクトを基本クラスポインタに割り当てます。
理想的には、「delete b」が呼び出されたときに呼び出されるデストラクタは、派生クラスのデストラクタである必要がありますが、出力から、基本クラスのデストラクタが基本クラスのポインタとして呼び出されることがわかります。
このため、派生クラスのデストラクタは呼び出されず、派生クラスのオブジェクトはそのまま残り、メモリリークが発生します。これに対する解決策は、基本クラスのコンストラクターを仮想化して、オブジェクトポインターが正しいデストラクタを指し、オブジェクトの適切な破棄が実行されるようにすることです。
仮想デストラクタの使用法を以下の例に示します。
オープンソースのクロスブラウザテストツール
#include using namespace std; class Base { public: virtual ~Base() { cout << 'Base Class:: Destructor
'; } }; class Derived:public Base { public: ~Derived() { cout<< 'Derived class:: Destructor
'; } }; int main() { Base* b = new Derived; // Upcasting delete b; }
出力:
派生クラス::デストラクタ
基本クラス::デストラクタ
これは、基本クラスのデストラクタの前に仮想キーワードを追加したことを除いて、前のプログラムと同じプログラムです。基本クラスのデストラクタを仮想化することで、目的の出力を実現しました。
派生クラスオブジェクトを基本クラスポインタに割り当ててから基本クラスポインタを削除すると、オブジェクト作成の逆の順序でデストラクタが呼び出されることがわかります。これは、最初に派生クラスのデストラクタが呼び出されてオブジェクトが破棄され、次に基本クラスのオブジェクトが破棄されることを意味します。
注意: C ++では、コンストラクターはオブジェクトの構築と初期化に関与するため、コンストラクターを仮想にすることはできません。したがって、すべてのコンストラクターを完全に実行する必要があります。
結論
ランタイムポリモーフィズムは、メソッドのオーバーライドを使用して実装されます。これは、それぞれのオブジェクトでメソッドを呼び出すときに正常に機能します。ただし、基本クラスポインターがあり、派生クラスオブジェクトを指す基本クラスポインターを使用してオーバーライドされたメソッドを呼び出すと、静的リンクが原因で予期しない結果が発生します。
これを克服するために、仮想関数の概念を使用します。 vtablesと_vptrの内部表現により、仮想関数は目的の関数を正確に呼び出すのに役立ちます。このチュートリアルでは、C ++で使用されるランタイムポリモーフィズムについて詳しく見てきました。
これで、C ++でのオブジェクト指向プログラミングに関するチュートリアルを終了します。このチュートリアルが、C ++でのオブジェクト指向プログラミングの概念をよりよく完全に理解するのに役立つことを願っています。
=> ゼロからC ++を学ぶには、ここにアクセスしてください。
推奨読書