ポリモーフィズムとは何ですか?その実装メカニズムは何ですか?オーバーロードと書き換えの違いは何ですか?今回検討する 4 つの非常に重要な概念は、継承、ポリモーフィズム、オーバーロード、および上書きです。
継承
簡単に言えば、継承とは、新しいメソッドを追加するか、既存のメソッドを再定義することによって、既存の型に基づいて新しい型を生成することです (後述するように、この方法は書き換えと呼ばれます)。継承はオブジェクト指向の 3 つの基本特性 (カプセル化、継承、ポリモーフィズム) の 1 つです。JAVA 言語では java.lang.Object クラスが最も基本的な基本クラスであるため、JAVA を使用するときに作成するすべてのクラスは継承します。新しく定義したクラスがどの基本クラスから継承するかを明示的に指定しない場合、JAVA はデフォルトで Object クラスから継承します。
JAVA のクラスは次の 3 つのタイプに分類できます。
クラス: class を使用して定義されたクラスであり、抽象メソッドは含まれません。
抽象クラス: 抽象クラスを使用して定義されたクラス。抽象メソッドが含まれる場合と含まれない場合があります。
インターフェース: インターフェースを使用して定義されたクラス。
これら 3 つのタイプの間には、次の継承ルールが存在します。
クラスはクラス、抽象クラスを拡張し、インターフェイスを実装できます。
抽象クラスはクラスを継承 (拡張) することができ、抽象クラスを継承 (拡張) することができ、インターフェースを継承 (実装) することができます。
インターフェイスはインターフェイスを拡張することしかできません。
上記 3 つのルールの各継承ケースで使用されるさまざまなキーワード extends およびimplement は、自由に置き換えることはできないことに注意してください。ご存知のとおり、通常のクラスはインターフェイスを継承した後、このインターフェイスで定義されているすべてのメソッドを実装する必要があります。実装しない場合は、抽象クラスとしてのみ定義できます。ここで、implements キーワードに「実装」という用語を使用しない理由は、概念的には継承関係も表しており、抽象クラスの実装インターフェイスの場合、このインターフェイス定義を実装する必要がないためです。したがって、継承を使用する方が合理的です。
上記の 3 つのルールは、次の制約にも準拠します。
クラスと抽象クラスはどちらも、最大 1 つのクラス、または最大 1 つの抽象クラスのみを継承できます。これら 2 つの状況は相互に排他的です。つまり、クラスまたは抽象クラスのいずれかを継承します。
クラス、抽象クラス、およびインターフェイスがインターフェイスを継承する場合、理論上は無制限の数のインターフェイスを継承できます。もちろん、クラスの場合、継承するすべてのインターフェイスで定義されているすべてのメソッドを実装する必要があります。
抽象クラスが抽象クラスを継承する場合、またはインターフェイスを実装する場合、親抽象クラスの抽象メソッドまたは親クラス インターフェイスで定義されたインターフェイスを部分的、完全、または完全に実装しない場合があります。
クラスが抽象クラスを継承するか、インターフェイスを実装する場合、そのクラスは、親抽象クラスのすべての抽象メソッド、または親クラス インターフェイスで定義されているすべてのインターフェイスを実装する必要があります。
継承がプログラミングにもたらす利点は、元のクラスの再利用 (再利用) です。モジュールの再利用と同様に、クラスの再利用によって開発効率が向上します。実際、モジュールの再利用は、多数のクラスの再利用の重畳的な効果です。継承に加えて、合成を使用してクラスを再利用することもできます。いわゆる組み合わせとは、元のクラスを新しいクラスの属性として定義し、元のクラスのメソッドを新しいクラスで呼び出すことで再利用を実現するものです。新しく定義された型と元の型の間に包含関係がない場合、つまり抽象的な概念から、新しく定義された型によって表されるものは、黄色の人など、元の型によって表されるものの 1 つではありません。それは人間の一種であり、それらの間には包含する、包含されるという関係があるため、現時点では再利用を実現するには結合する方が良い選択です。次の例は、組み合わせの簡単な例です。
public class Sub { private Parent p = new Parent(); public void doSomething() { // Parent クラスのメソッド p.method() を再利用 } } class Parent { public void method() { / / ここで何かをします } }
もちろん、コードをより効率的にするために、元の型 (Parent p など) を使用する必要がある場合にコードを初期化することもできます。
継承と結合を使用して元のクラスを再利用することは、インクリメンタル開発モデルです。この方法の利点は、元のコードを変更する必要がないため、元のコードに新たなバグが発生しないことです。元のコードの変更による再テストは、開発にとって明らかに有益です。したがって、元のシステムやモジュールを保守または変更する場合、特にそれらを完全に理解していない場合は、増分開発モデルを選択できます。これにより、開発効率が大幅に向上するだけでなく、システムやモジュールによって引き起こされるリスクも回避できます。元のコードへの変更。
ポリモーフィズム
ポリモーフィズムは、前述したように、オブジェクト指向の 3 つの基本特性の 1 つです。ポリモーフィズムとは一体何でしょうか?理解を助けるために、まず次の例を見てみましょう。
// 車のインターフェイス インターフェイス Car { // 車の名前 String getName(); // 車の価格を取得します int getPrice(); } // BMW クラス BMW を実装します { public String getName() { return "BMW"; getPrice() { return 300000; } } // CheryQQ クラス CheryQQ は Car { public String getName() { return "CheryQQ" } public int getPrice(); { return 20000; } } // 自動車販売店 public class CarShop { // 自動車販売収入 private int Money = 0 // 車を販売 public void sellCar(Car car) { System.out.println("Car model: " + car.getName() + " 単価: " + car.getPrice()) // 自動車販売収入の増加 += car.getPrice() } // 自動車販売収入の合計 public int; getMoney() { お金を返す; } public static void main(String[] args) { CarShop aShop = new CarShop() // BMW を販売する aShop.sellCar(new BMW()); // BMW Chery QQ を販売する.sellCar(new CheryQQ()); System.out.println("総収益: " + aShop.getMoney());
実行結果:
車種:BMW 本体価格:300,000
モデル: CheryQQ 単価: 20,000
総収入: 320,000
継承はポリモーフィズムを実現するための基礎です。文字通り理解すると、ポリモーフィズムは複数の状態を示す型 (両方の Car 型) です (BMW の名前は BMW、価格は 300,000、奇瑞の名前は CheryQQ、価格は 2,000)。メソッド呼び出しを、そのメソッドが属するサブジェクト (つまり、オブジェクトまたはクラス) に関連付けることをバインディングと呼びます。バインディングは、早期バインディングと遅延バインディングの 2 つのタイプに分類されます。それらの定義を以下に説明します。
早期バインディング: プログラムが実行される前のバインディング。コンパイラーとリンカーによって実装され、静的バインディングとも呼ばれます。たとえば、静的メソッドと最終メソッドは暗黙的に最終メソッドであるため、ここに含まれることに注意してください。
遅延バインディング: 実行時のオブジェクトの型に応じたバインディング。メソッド呼び出しメカニズムによって実装されるため、動的バインディングまたはランタイム バインディングとも呼ばれます。早期バインディングを除くすべてのメソッドは遅延バインディングです。
ポリモーフィズムは遅延バインディングのメカニズムに実装されています。ポリモーフィズムがもたらす利点は、クラス間の結合関係がなくなり、プログラムの拡張が容易になることです。たとえば、上記の例では、販売用の新しいタイプの車を追加するには、元のコードを変更せずに、新しく定義したクラスに Car クラスを継承させ、そのすべてのメソッドを実装するだけで済みます。 CarShop クラスの ) メソッドは新しい車種を扱うことができます。新しいコードは次のとおりです。
// Santana Car クラス Santana は Car を実装します { public String getName() { return "Santana" } public int getPrice() { return 80000; }
オーバーロードとオーバーライド
オーバーロードと書き換えはどちらもメソッドの概念です。これら 2 つの概念を明確にする前に、まずメソッドの構造が何であるかを理解しましょう (英語名は Signature ですが、一部では「署名」と訳されますが、より広く使用されていますが、この翻訳はそうではありません)。正確な)。構造とは、メソッドの構成構造を指します。具体的には、メソッドの名前とパラメータ、パラメータの数、タイプ、出現順序をカバーしますが、メソッドの戻り値のタイプ、アクセス修飾子、および変更は含まれません。抽象シンボル、静的シンボル、最終シンボルなど。たとえば、次の 2 つのメソッドは同じ構造を持っています。
public void method(int i, String s) { // 何かをする } public String Method(int i, String s) { // 何かをする }
これら 2 つは、構成が異なるメソッドです。
public void method(int i, String s) { // 何かをする } public void method(String s, int i) { // 何かをする }
ゲシュタルトの概念を理解した後、オーバーロードと書き換えについて見てみましょう。その定義を見てください。
オーバーライドとは、英語名が overriding で、継承の際に、基底クラスのメソッドと同じ構造を持つ新しいメソッドをサブクラスに定義することを意味し、これを基底クラスのメソッドをオーバーライドするサブクラスと呼びます。これはポリモーフィズムを実現するために必要なステップです。
オーバーロード(英語名は overloading )とは、同じクラス内に同じ名前で構造が異なる複数のメソッドを定義することを指します。同じクラス内で、同じ型の複数のメソッドを定義することはできません。
興味深い質問について考えてみましょう: コンストラクターはオーバーロードできますか?答えはもちろん「はい」です。実際のプログラミングではこれをよく行います。実際、コンストラクターもメソッドです。コンストラクター名はメソッド名であり、コンストラクターのパラメーターはメソッドのパラメーターであり、その戻り値は新しく作成されたクラスのインスタンスです。ただし、サブクラスは基本クラスと同じ型のコンストラクターを定義できないため、コンストラクターをサブクラスでオーバーライドすることはできません。
オーバーロード、オーバーライド、ポリモーフィズム、関数の隠蔽
C++ の初心者の中には、オーバーロード、上書き、ポリモーフィズム、関数の隠蔽について漠然と理解している人もいます。ここでは、C++ 初心者の疑問を解消するために私自身の意見をいくつか書きます。
オーバーロード、上書き、ポリモーフィズム、関数の隠蔽の間の複雑かつ微妙な関係を理解する前に、まずオーバーロードやカバレッジなどの基本的な概念を確認する必要があります。
まず、関数の非表示とは何かを理解するために、非常に簡単な例を見てみましょう。
#include <iostream>using namespace std;class Base{public: void fun() { cout << "Base::fun()" << endl }};class Derive : public Base{public: void fun(int i; ) { cout << "Derive::fun()" << endl; }};int main(){ Derive d; //次の文は間違っているためブロックされます //d.fun();error C2660 : 'fun' : 関数は 0 パラメータを取りません d.fun(1); Derive *pd =new Derive(); //次の文は間違っているため、ブロックされます //pd->fun();error C2660: 'fun' : 関数は 0 パラメータを取りません pd->fun(1) delete pd;}
/*異なる非名前空間スコープ内の関数はオーバーロードを構成しません。サブクラスと親クラスは 2 つの異なるスコープです。
この例では、2 つの関数は異なるスコープ内にあるため、スコープが名前空間スコープでない限り、オーバーロードされません。 */
この例では、関数はオーバーロードまたはオーバーライドされず、非表示になります。
次の 5 つの例では、隠蔽とは何かを具体的に説明します。
例1
#include <iostream>using namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//オーバーロード void fun(int i){cout << "Base ::fun(int i)" << endl;}//オーバーロード};class Derive :public Basic{public: void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d.fun();//正解です。派生クラスには基本クラスと同じ名前の関数宣言がありません。その場合、同じ名前を持つすべてのオーバーロードされた関数が含まれます。基本クラスは候補関数として使用されます。 d.fun(1);//正解です。派生クラスには基本クラスと同じ名前の関数宣言がありません。基本クラス内の同じ名前を持つすべてのオーバーロードされた関数が候補関数として使用されます。 0 を返します;}
例 2
#include <iostream>using namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//オーバーロード void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //新しい関数バージョン。基本クラスのオーバーロードされたバージョンはすべてブロックされます。ここでは、それを非表示関数と呼びます。 //派生クラス内に基底クラスと同名の関数の宣言がある場合、基底クラスに複数のバージョンがあっても、基底クラス内の同名の関数は候補関数として使用されません異なるパラメータリストを持つオーバーロードされた関数。 void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;} void fun2(){cout << "Derive::fun2()" << endl ;}};int main(){ Derive d; d.fun(1,2); //次の文は間違っているため、ブロックされます //d.fun();error C2660: 'fun' : 関数は実行します0 パラメータを取らない0 を返します;}
例 3
#include <iostream>using namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//オーバーロード void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //オーバーライド基本クラスの関数バージョンの 1 つをオーバーライドします。同様に、基本クラスのすべてのオーバーロードされたバージョンは次のようになります。隠れた。 //派生クラス内に基底クラスと同名の関数の宣言がある場合、基底クラスに複数のバージョンがあっても、基底クラス内の同名の関数は候補関数として使用されません異なるパラメータリストを持つオーバーロードされた関数。 void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d.fun(); //次の文は間違っているため、ブロックされます //d.fun(1);エラー C2660: 'fun' : 関数は 1 つのパラメータを受け取りません;}
例 4
#include <iostream>using namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//オーバーロード void fun(int i){cout << "Base ::fun(int i)" << endl;}//オーバーロード};class Derive :public Basic{public: using Basic::fun; void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d.fun();//d.fun(1) を修正します。 //正しい return 0;}/*出力結果 Derive::fun()Base::fun(int i)続行するには任意のキーを押してください*/
例5
#include <iostream>using namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//オーバーロード void fun(int i){cout << "Base ::fun(int i)" << endl;}//オーバーロード};class Derive :public Basic{public: using Basic::fun; void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; .fun();//d.fun(1)を修正;//d.fun(1,2)を修正;//return 0を修正;}/*出力結果Base::fun()Base::fun(int i)Derive::fun(int i,int j)続行するには任意のキーを押してください*/
それでは、まずオーバーロードと上書きの特徴を簡単にまとめてみましょう。
過負荷の特徴:
n 同じスコープ(同じクラス内)。
n 関数名は同じですが、パラメータが異なります。
n virtual キーワードはオプションです。
オーバーライドは、派生クラス関数が基本クラス関数をカバーすることを意味します。オーバーライドの特徴は次のとおりです。
n 異なるスコープ (それぞれ派生クラスと基本クラスにあります)。
n 関数名とパラメータは同じです。
n 基本クラス関数には、virtual キーワードが必要です。 (仮想キーワードがない場合は非表示と呼ばれます)
基本クラスに関数の複数のオーバーロードされたバージョンがあり、派生クラスの基本クラスの 1 つ以上の関数バージョンをオーバーライド (上書き) するか、派生クラスの関数バージョンに新しいバージョンを追加する場合 (同じ関数名、異なるパラメーター)の場合、基本クラスのすべてのオーバーロードされたバージョンがブロックされます。これをここでは非表示と呼びます。したがって、一般に、派生クラスで新しい関数バージョンを使用し、基本クラスの関数バージョンを使用したい場合は、基本クラス内のすべてのオーバーロードされたバージョンを派生クラスでオーバーライドする必要があります。基本クラスのオーバーロードされた関数バージョンをオーバーライドしたくない場合は、例 4 または例 5 を使用して、基本クラスの名前空間スコープを明示的に宣言する必要があります。
実際、C++ コンパイラーは、同じ関数名と異なるパラメーターを持つ関数間には関係がないと考えており、これらは単に 2 つの無関係な関数であると考えられます。 C++ 言語では、現実世界をシミュレートし、プログラマーが現実世界の問題をより直感的に処理できるようにするために、オーバーロードと上書きの概念が導入されました。オーバーロードは同じ名前空間スコープの下にありますが、オーバーライドは異なる名前空間スコープの下にあります。たとえば、基本クラスと派生クラスは 2 つの異なる名前空間スコープです。継承プロセス中に、派生クラスが基本クラス関数と同じ名前を持つ場合、基本クラス関数は非表示になります。もちろん、ここで説明する状況は、基本クラス関数の前に仮想キーワードがないということです。仮想キーワードキーワードが存在する場合の状況については、別途説明します。
継承されたクラスは、基本クラスの関数バージョンをオーバーライドして、独自の関数インターフェイスを作成します。このとき、C++ コンパイラは、派生クラスの書き換えられたインターフェイスを使用したいため、基本クラスのインターフェイスは提供されないと判断します (もちろん、名前空間スコープを明示的に宣言する方法を使用することもできます)。 [C++ の基本] オーバーロード、上書き、ポリモーフィズム、および関数の隠蔽 (1)) を参照してください。基本クラスのインターフェースにオーバーロード特性があることは無視されます。派生クラスでオーバーロード機能を維持し続けたい場合は、インターフェイスのオーバーロード機能を自分で提供します。したがって、派生クラスでは、関数名が同じである限り、基本クラスの関数バージョンは容赦なくブロックされます。コンパイラでは、マスクは名前空間スコープを通じて実装されます。
したがって、基本クラス関数のオーバーロードされたバージョンを派生クラスで維持するには、基本クラスのすべてのオーバーロードされたバージョンをオーバーライドする必要があります。オーバーロードは現在のクラスでのみ有効であり、継承により関数のオーバーロードの特性が失われます。つまり、基底クラスのオーバーロードされた関数を継承された派生クラスに配置したい場合は、それを書き直す必要があります。
ここでの「非表示」とは、派生クラスの関数が同じ名前の基本クラスの関数をブロックすることを意味します。具体的なルールについても簡単にまとめておきます。
n 派生クラスの関数の名前が基本クラスの関数と同じであるが、パラメーターが異なる場合。このとき、基底クラスにvirtualキーワードが存在しない場合、基底クラスの機能は非表示になります。 (オーバーロードと混同しないように注意してください。同じ名前でパラメータが異なる関数はオーバーロードと呼ばれるはずですが、派生クラスと基底クラスが同じ名前空間スコープにないため、ここではオーバーロードとして理解できません。これは隠れていると理解されます)
n 派生クラスの関数の名前が基本クラスの関数と同じであるが、パラメーターが異なる場合。このとき、基底クラスにvirtualキーワードがあれば、基底クラスの機能が暗黙的に派生クラスのvtableに継承されます。このとき、派生クラス vtable 内の関数は、基底クラスのバージョンの関数アドレスを指します。同時に、この新しい関数バージョンは、派生クラスのオーバーロードされたバージョンとして派生クラスに追加されます。ただし、基本クラス ポインターが多態性呼び出し関数メソッドを実装する場合、この新しい派生クラス関数のバージョンは非表示になります。
n 派生クラスの関数が基本クラスの関数と同じ名前を持ち、パラメータも同じであるが、基本クラスの関数に virtual キーワードがない場合。このとき、基底クラスの関数は隠蔽されます。 (これをカバレッジと混同しないように注意してください。ここでは、カバレッジは非表示として理解されます)。
n 派生クラスの関数が基本クラスの関数と同じ名前を持ち、パラメータも同じであるが、基本クラスの関数に virtual キーワードがある場合。このとき、基底クラスの関数は「隠蔽」されません。 (ここでは、報道として理解する必要があります^_^)。
間奏: 基本クラス関数の前に仮想キーワードがない場合は、それをオーバーライドと呼ぶ方がよりスムーズに書き換えられることを望みます。誰もが C++ をよりよく理解できるようになります。早速、例を挙げて説明しましょう。
例6
#include <iostream>using namespace std; class Base{public: virtual void fun() { cout << "Base::fun()" << endl }//オーバーロード virtual void fun(int i) { cout << "Base::fun(int i)" << endl; }//オーバーロード}; class Derive : public Base{public: void fun() { cout << "Derive::fun()" << endl; }//オーバーライド void fun(int i) { cout << "Derive::fun(int i)" << endl; }//オーバーライド void fun(int i,int j){ cout<< "Derive::fun (int i,int j)" <<endl;}//オーバーロード}; int main(){ Base *pb = new Derive(); pb->fun(); pb->fun(1); //次の文は間違っているためブロックされます //pb->fun(1,2); 仮想関数はオーバーロードできません、エラー C2661: 'fun' : オーバーロードされた関数は 2 つのパラメーターを受け取りません cout << endl; pd = new Derive(); pd->fun(1); // オーバーロード pd を削除; }/*
出力結果
派生::fun()
派生::fun(int i)
派生::fun()
派生::fun(int i)
派生::fun(int i,int j)
続行するには任意のキーを押してください
*/
例7-1
#include <iostream> 名前空間 std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; int main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) delete 0;}
例7-2
#include <iostream> 名前空間 std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(double d){ cout <<"Derive::fun(double d)"<< endl; int main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) pb->fun((double)0.01);//Base::fun(int i) delete pb;}
例8-1
#include <iostream> 名前空間 std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(int i){ cout <<"Derive::fun(int i)"<< endl }}; int main(){ Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) delete pb; }
例8-2
#include <iostream> 名前空間 std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(int i){ cout <<"Derive::fun(int i)"<< endl; } void fun(double d){ cout <<"Derive::fun(double; d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) pb->fun((double) 0.01);//Derive::fun(int i) 削除 pb 0;}
例9
#include <iostream> 名前空間 std を使用します; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }};class Derive : public Base{public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl; } void fun(char c){ cout <<"Derive::fun(char c)"<< endl; void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } };int main(){ Base *pb = new Derive(); //Derive::fun(int i) pb->fun('a');//Derive::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) Derive *pd =new Derive(); pd->fun(1);//Derive::fun(int i) //オーバーロード pd->fun('a');//Derive::fun(char c) //オーバーロード pd->fun(0.01);//Derive::fun(double d) pb を削除; pd を削除; }
例 7-1 と 8-1 は、誰もが比較できるように、また理解を深められるように、ここに 2 つの例を示しています。
■ 例 7-1 では、派生クラスは基底クラスの仮想関数をカバーしていません。このとき、派生クラスの vtable 内の関数ポインタが指すアドレスは、基底クラスの仮想関数のアドレスになります。
■ 例 8-1 では、派生クラスが基本クラスの仮想関数をオーバーライドしています。このとき、派生クラスの vtable 内の関数ポインタが指すアドレスは、派生クラス自身のオーバーライドされた仮想関数のアドレスです。
例 7-2 と 8-2 は少し奇妙に見えますが、実際、上記の原則に従って比較すると、答えは明らかです。
n 例 7-2 では、派生クラスの関数バージョンをオーバーロードしました: void fun(double d) 実際、これは単なる隠蔽です。具体的に分析してみましょう。基本クラスにはいくつかの関数があり、派生クラスにはいくつかの関数があります。
型の基本クラスの派生クラス
Vテーブルセクション
void fun(int i)
仮想関数 void fun(int i) の基本クラス バージョンを指します。
静的部分
void fun(ダブルd)
次の 3 行のコードをもう一度分析してみましょう。
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
最初の文がキーであり、基底クラス ポインタは派生クラスのオブジェクトを指します。これは多態性呼び出しであることがわかります。実行時の基底クラス ポインタは、以下に基づいています。ランタイム オブジェクトの型なので、まず派生クラスの vtable に移動して、派生クラスの仮想関数のバージョンを見つけます。派生クラスが基本クラスの vtable をカバーしていないことがわかります。派生クラスは基本クラスの仮想関数のアドレスへのポインターを作成するだけなので、仮想関数の基本クラス バージョンを呼び出すのは自然です。最後の文では、プログラムは依然として派生クラスの vtable を探していますが、このバージョンの仮想関数がまったく存在しないことが判明したため、戻って独自の仮想関数を呼び出す必要があります。
ここで、この時点で基底クラスに複数の仮想関数がある場合、プログラムのコンパイル時に「不明瞭な呼び出し」というメッセージが表示されることにも注意してください。例は次のとおりです
#include <iostream> 名前空間 std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl } virtual void fun(char c){ cout < <"Base::fun(char c)"<< endl }}; class Derive : public Base{public: void fun(double d){ cout <<"Derive::fun(double; d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(0.01);//エラー C2668: 'fun' : オーバーロードされた関数へのあいまいな呼び出し delete pb; return 0;}
さて、例 8-2 をもう一度分析しましょう。
n 例 8-2 では、派生クラスの関数バージョン void fun(double d) もオーバーロードし、基本クラスの仮想関数についても説明しました。基本クラスにはいくつかの関数があります。 、派生クラスにはいくつかの関数があります。
型の基本クラスの派生クラス
Vテーブルセクション
void fun(int i)
void fun(int i)
静的部分
void fun(ダブルd)
テーブルから、派生クラスの vtable 内の関数ポインターが、オーバーライドされた独自の仮想関数アドレスを指していることがわかります。
次の 3 行のコードをもう一度分析してみましょう。
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
1 番目の文については特に説明する必要はありません。2 番目の文は、当然のことながら派生クラスの仮想関数バージョンを呼び出します。実際、C++ プログラムは非常に奇妙に感じます。実行中、派生クラスの vtable テーブルを見たところ、本当に必要なバージョンがないことに気づきました。 、なぜ基底クラスのポインタが周りを見回してそれを探さないのですか? はは、基底クラスの視力は非常に古いので、その目で見えるのはそれ自身の目だけです。 Vtable部分(つまり静的部分)と管理したいVtable部分、派生クラスのvoid fun(double d) は遠すぎて見えません! それに、派生クラスがすべてを処理する必要があります。派生クラスには独自の権限があるのではないでしょうか。もう議論する必要はありません。自分自身を大事にしてください^_^
ああ、ため息をつくつもりですか? 基本クラス ポインターはポリモーフィックな呼び出しを行うことはできますが、派生クラスへのオーバーロード呼び出しを行うことはできません (例 6 を参照)。
もう一度例 9 を見てみましょう。
この例の効果は、同じ目的を持つ例 6 の効果と同じです。上記の例を理解すると、これもちょっとしたキスであると思います。
まとめ:
オーバーロードでは、関数のパラメーター リストに基づいて呼び出される関数のバージョンが選択されますが、ポリモーフィズムでは、ランタイム オブジェクトの実際の型に基づいて呼び出される仮想関数のバージョンが選択されます。ポリモーフィズムは、仮想仮想クラスへの派生クラスを通じて実装されます。派生クラスが基本クラスの仮想仮想関数をオーバーライドしない場合、派生クラスは基本クラスの仮想仮想関数を自動的に継承します。関数のバージョン。このとき、基底クラスのポインターが指すオブジェクトが基底型であるか派生型であるかに関係なく、派生クラスが仮想仮想関数をオーバーライドする場合は、基底クラスのバージョンの仮想仮想関数が呼び出されます。たとえば、基本クラス ポインタが指すオブジェクト タイプが派生タイプの場合、オブジェクトの実際のタイプは、呼び出される仮想仮想関数のバージョンを選択するために使用されます。 、派生クラスの仮想仮想関数バージョンが呼び出され、ポリモーフィズムが実現されます。
ポリモーフィズムを使用する本来の目的は、基本クラスで関数を仮想として宣言し、派生クラスで基本クラスの仮想仮想関数バージョンをオーバーライドすることです。この時点の関数プロトタイプは基本クラスと一致していることに注意してください。つまり、同じ名前と同じ名前のパラメーター型です。新しい関数バージョンを派生クラスに追加した場合、この新しい関数バージョンは、基本クラス ポインターを通じて動的に呼び出すことはできません。派生クラスのオーバーロードされたバージョンとして。同じ文ですが、オーバーロードは現在のクラスでのみ有効であり、基本クラスでオーバーロードするか派生クラスでオーバーロードするかにかかわらず、この 2 つは相互に関連しません。これを理解すれば、例 6 と 9 の出力結果も正常に理解できます。
オーバーロードは静的にバインドされ、ポリモーフィズムは動的にバインドされます。さらに説明すると、オーバーロードはポインターが実際に指すオブジェクトの型とは関係がなく、ポリモーフィズムはポインターが実際に指すオブジェクトの型に関係します。基本クラス ポインターが派生クラスのオーバーロードされたバージョンを呼び出す場合、C++ コンパイラーは、基本クラス ポインターが基本クラスのオーバーロードされたバージョンのみを呼び出すことができ、オーバーロードは名前空間でのみ機能するとみなします。現在のクラスのドメイン内で有効な場合、この時点で基本クラスのポインターが仮想関数を呼び出すと、当然ながらオーバーロード機能が失われます。次に、基本クラスの仮想仮想関数のバージョンまたは派生クラスの仮想仮想関数のバージョンを動的に選択して、特定の操作を実行します。これは、基本クラスのポインターが実際に指すオブジェクトのタイプによって決定されるため、オーバーロードとポインターが決まります。ポインターが実際に指すオブジェクトのタイプは関係ありません。ポリモーフィズムは、ポインターが実際に指すオブジェクトのタイプに関係します。
最後に、仮想仮想関数もオーバーロードできますが、オーバーロードは現在の名前空間の範囲内でのみ有効です。 String オブジェクトはいくつ作成されていますか?
まずコードの一部を見てみましょう。
Javaコード
文字列 str=new String("abc");
このコードの後には、多くの場合、「このコード行によって作成される String オブジェクトの数は何ですか?」という質問が続きます。この質問は皆さんよくご存じだと思いますし、答えもよく知られています、 2.次に、この質問から始めて、String オブジェクトの作成に関連する Java の知識を確認していきます。
上記のコード行は、String str、=、"abc"、および new String() の 4 つの部分に分割できます。 String str は str という名前の String 型変数を定義するだけなので、オブジェクトは作成されません。 = 変数 str を初期化し、それにオブジェクトへの参照 (またはハンドル) を割り当てます。また、明らかにオブジェクトは作成されません。文字列("abc")は残ります。では、なぜ new String("abc") は "abc" と new String() としてみなされるのでしょうか?呼び出した String コンストラクターを見てみましょう。
Javaコード
public String(元の文字列) {
//他のコード...
}
ご存知のとおり、クラスのインスタンス (オブジェクト) を作成するには、一般的に 2 つの方法が使用されます。
オブジェクトを作成するには new を使用します。
Class クラスの newInstance メソッドを呼び出し、リフレクション メカニズムを使用してオブジェクトを作成します。
new を使用して String クラスの上記のコンストラクター メソッドを呼び出し、オブジェクトを作成し、その参照を str 変数に割り当てました。同時に、呼び出されたコンストラクター メソッドによって受け入れられるパラメーターも String オブジェクトであり、このオブジェクトは正確に「abc」であることに気付きました。これから、String オブジェクト、つまり引用符で囲まれたテキストを作成する別の方法を導入する必要があります。
このメソッドは String に固有であり、新しいメソッドとは大きく異なります。
Javaコード
文字列 str="abc";
このコード行で String オブジェクトが作成されることは間違いありません。
Javaコード
文字列 a="abc";
文字列 b="abc";
ここはどうですか?答えはまだ一つです。
Javaコード
文字列 a="ab"+"cd";
ここはどうですか?答えはまだ一つです。ちょっと変ですか?この時点で、文字列プール関連の知識を復習する必要があります。
JAVA 仮想マシン (JVM) には文字列プールがあり、多くの文字列オブジェクトを保存し、共有できるため、効率が向上します。 String クラスは最終クラスであるため、作成後にその値を変更することはできないため、String オブジェクトの共有によって引き起こされるプログラムの混乱を心配する必要はありません。文字列プールは String クラスによって維持され、intern() メソッドを呼び出して文字列プールにアクセスできます。
String a="abc"; を見てみましょう。このコード行が実行されると、JAVA 仮想マシンはまず文字列プール内を検索し、値が「abc」であるオブジェクトが既に存在するかどうかを判断します。 Stringクラスのequals(Object obj)メソッドの戻り値。存在する場合、新しいオブジェクトは作成されず、既存のオブジェクトへの参照が直接返されます。存在しない場合は、最初にオブジェクトが作成され、次に文字列プールに追加されてから、その参照が返されます。したがって、前の 3 つの例のうち最初の 2 つがなぜこの答えになるのかを理解するのは難しくありません。
3 番目の例の場合:
Javaコード
文字列 a="ab"+"cd";
定数の値はコンパイル時に決定されるためです。ここで、「ab」と「cd」は定数であるため、変数 a の値はコンパイル時に決定できます。このコード行のコンパイルされた効果は次と同等です。
Javaコード
文字列 a="abcd";
したがって、ここではオブジェクト「abcd」を 1 つだけ作成し、文字列プールに保存します。
ここで再び質問になりますが、「+」接続後に取得されたすべての文字列は文字列プールに追加されるのでしょうか? 「==」を使用して 2 つの変数を比較できることは、次の 2 つの状況で使用できることは誰もが知っています。
2 つの基本型 (char、byte、short、int、long、float、double、boolean) を比較した場合、それらの値が等しいかどうかが判断されます。
テーブルが 2 つのオブジェクト変数を比較する場合、それらの参照が同じオブジェクトを指しているかどうかが判断されます。
次に、「==」を使用していくつかのテストを実行します。説明を簡単にするために、文字列プールにすでに存在するオブジェクトを指すことを、文字列プールに追加されるオブジェクトとみなします。
Javaコード
public class StringTest { public static void main(String[] args) { String a = "ab";// オブジェクトを作成し、文字列プールに追加します System.out.println("String a = /"ab/" ; "); String b = "cd";// オブジェクトが作成され、文字列プールに追加されます System.out.println("String b = /"cd/";"); String c = "abcd"; //オブジェクトが作成され、文字列プールに追加されます。 String d = "ab" + "cd" // d と c が同じオブジェクトを指している場合、d も文字列プールに追加されたことを意味します if (d ==; c ) { System.out.println("/"ab/"+/"cd/" 作成されたオブジェクトは/" 文字列プールに追加されます") } // d と c が同じを指していない場合オブジェクト、これは d が文字列プールに追加されていないことを意味します。 else { System.out.println("/"ab/"+/"cd/" 作成されたオブジェクトは/"文字列プールに追加されません/") } String e = a + "cd"; c は同じオブジェクトを指します。これは、e も文字列プールに追加されたことを意味します if (e == c) { System.out.println(" a +/"cd/" 作成されたオブジェクト/"結合/" 文字列プール中央"); } // e と c が同じオブジェクトを指していない場合は、e が文字列プールに追加されていないことを意味します else { System.out.println(" a +/"cd/" created object/"not added/" to the string pool" ); } String f = "ab" + b; // f と c が同じオブジェクトを指している場合、f も文字列プールに追加されていることを意味します if (f == c) { System.out .println("/ "ab/"+ b によって作成されたオブジェクト/"Added/" to the string pool"); } // f と c が同じオブジェクトを指していない場合、f が文字列プールに追加されていないことを意味します else { System.out.println("/" ab/" + b created object/"not added/" to the string pool"); } String g = a + b; // g と c が同じオブジェクトを指している場合、g も追加されていることを意味します。 string pool if ( g == c) { System.out.println(" a + b 作成されたオブジェクト/"added/" を文字列プールに"); } // g と c が同じオブジェクトを指していない場合は、g が文字列プールに追加されていないことを意味します。 else { System.out.println (" a + b はオブジェクトを作成しました/"文字列プールに追加されません/" } } }
実行結果は次のとおりです。
文字列 a = "ab";
文字列 b = "cd";
「ab」+「cd」によって作成されたオブジェクトは文字列プールに「結合」されます。
+ "cd" によって作成されたオブジェクトは文字列プールに「追加されません」。
「ab」+ b によって作成されたオブジェクトは文字列プールに「追加されません」。
a + b によって作成されたオブジェクトは文字列プールに「追加されません」。上記の結果から、テキストを含めるために引用符を使用して作成された String オブジェクト間の「+」接続を使用して生成された新しいオブジェクトのみが追加されることが簡単にわかります。文字列プール内。新しいモードで作成されたオブジェクト (null を含む) を含むすべての「+」接続式では、生成される新しいオブジェクトは文字列プールに追加されません。これについては詳しく説明しません。
しかし、注意を要する状況が 1 つあります。以下のコードを見てください。
Javaコード
public class StringStaticTest { // 定数 A public static Final String A = "ab"; // 定数 B public static void main(String[] args) { // 2 つの定数を使用します。 s を初期化するには String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s は t に等しい、それらは同じオブジェクトです"); { System.out.println("s は t と等しくありません。同じオブジェクトではありません") } }
このコードを実行した結果は次のようになります。
s は t と同じです。なぜですか?その理由は、定数の値は固定されているため、コンパイル時に決定できますが、変数の値は実行時にのみ決定できます。この変数はさまざまなメソッドで呼び出すことができるため、次のような問題が発生する可能性があります。変更する値。上記の例では、AとBは定数でその値が固定されているため、sの値も固定であり、クラスのコンパイル時に決定されます。つまり、次のようになります。
Javaコード
文字列 s=A+B;
以下と同等:
Javaコード
文字列 s="ab"+"cd";
上記の例を少し変更して、何が起こるか見てみましょう。
Javaコード
public class StringStaticTest { // 定数 A public static Final String A; // 定数 B public static Final String B; } public static void main(String[] args); // 2 つの定数を + String s = A + B で接続して s を初期化します if (s == t); System.out.println("s は t に等しい、それらは同じオブジェクトです"); } else { System.out.println("s は t に等しくありません、それらは同じオブジェクトではありません"); } }
その操作の結果は次のようになります。
s は t と等しくありません。これらは同じオブジェクトではありませんが、結果は先ほどの例とはまったく逆になります。もう一度分析してみましょう。 A と B は定数として定義されていますが (1 回のみ割り当て可能)、すぐには割り当てられません。 s の値が計算される前、それらがいつ割り当てられるか、どのような値が割り当てられるかはすべて変数です。したがって、A と B は、値が割り当てられる前は変数のように動作します。この場合、 はコンパイル時に決定できず、実行時にのみ作成できます。
文字列プール内のオブジェクトを共有すると効率が向上するため、テキストを引用符で囲んで String オブジェクトを作成することをお勧めします。実際、これはプログラミングでよく使用される方法です。
次に、次のように定義されている intern() メソッドを見てみましょう。
Javaコード
パブリック ネイティブ String intern();
これはネイティブメソッドです。このメソッドを呼び出すと、JAVA 仮想マシンはまず、そのオブジェクトと同じ値を持つオブジェクトが文字列プールに存在するかどうかを確認し、存在する場合は、そのオブジェクトへの参照を文字列プールに返します。文字列プール内のオブジェクトを同じ値を持つ String オブジェクトとして取得し、その参照を返します。
このコードを見てみましょう:
Javaコード
public class StringInternTest { public static void main(String[] args) { // char 配列を使用して a を初期化し、値 "abcd" を持つオブジェクトが a が作成される前に文字列プールにすでに存在することを回避します String a = new String ( new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern() if (b == a); System.out.println("b は文字列プールに追加されませんでした。新しいオブジェクトは作成されませんでした"); } else { System.out.println("b は文字列プールに追加されませんでした。新しいオブジェクトは作成されませんでした"); } } }
実行結果:
b が文字列プールに追加されておらず、新しいオブジェクトが作成されている場合、String クラスの intern() メソッドが同じ値を持つオブジェクトを見つけられない場合、現在のオブジェクトを文字列プールに追加して、そのオブジェクトを返します。参照の場合、b と a は同じオブジェクトを指します。それ以外の場合、b が指すオブジェクトは Java 仮想マシンによって文字列プール内に新しく作成されますが、その値は a と同じです。上記のコードの実行結果は、この点を裏付けるものです。
最後に、JAVA 仮想マシン (JVM) 内の String オブジェクトのストレージと、文字列プールとヒープおよびスタックの関係について説明します。まず、ヒープとスタックの違いを確認してみましょう。
スタック: 主に基本型 (または組み込み型) (char、byte、short、int、long、float、double、boolean) とオブジェクト参照を保存します。データは共有でき、その速度は登録に次ぐものです。ヒープ。
ヒープ: オブジェクトを保存するために使用されます。
String クラスのソース コードを見ると、String オブジェクトの値を格納する value 属性があることがわかります。これは、文字列が文字のシーケンスであることも示しています。
String a="abc"; を実行すると、JAVA 仮想マシンはスタックに 3 つの char 値「a」、「b」、「c」を作成し、ヒープに String オブジェクト、その値 (value ) を作成します。スタック上に作成された 3 つの char 値の配列 {'a', 'b', 'c'} 最後に、新しく作成された String オブジェクトが文字列プールに追加されます。次に String b=new String("abc"); コードを実行すると、「abc」が作成されて文字列プールに保存されているため、JAVA 仮想マシンはヒープ内に新しい String オブジェクトを作成するだけですが、 value は、コードの前の行が実行されたときにスタック上に作成された 3 つの char 型の値 'a'、'b'、および 'c' です。
この時点で、この記事の冒頭で取り上げた String str=new String("abc") がなぜ 2 つのオブジェクトを作成するのかという疑問はすでにかなり明確になっています。