C++中の多状態と多重継承実現とJavaの違い


多形問題
学校の面接で有名な質問をされました。「C++とJavaはどうやって多状態を実現しますか?」あまりにも有名なので、かえって準備していません。虚函数表と関係があると知っています。面接の後、C++とJava多状態の実現の違いを比較しました。ここに記録します。
C++多状態の虚ポインタを実現します。
まず、C++.多形、すなわち、サブクラスが親クラスのメンバー関数を書き換えた後、サブクラスのポインタを親クラスに割り当て、この親類のポインタにメンバー関数を呼び出して、サブクラスの書き換えバージョンのメンバー関数を呼び出します。簡単な例:

class Parent1 {
  public:
  virtual void sayHello() { printf("Hello from parent1!
"); } }; class Child : public Parent1 { public: virtual void sayHello() { printf("Hello from child!
"); } }; int main() { Parent1 *p = new Child(); p->sayHello(); // get "Hello from child!" }
まず、下の実装において、メンバ関数は最初のパラメータがオブジェクトポインタの関数であり、コンパイラは自動的にオブジェクトポインタを関数パラメータに追加して、thisポインタと名づけます。これ以外の一般関数とは本質的な違いはありません。非多形のメンバ関数呼び出しは、基本的に非メンバ関数呼び出しプロセスと一致し、パラメータリスト(パラメータリストにオブジェクトポインタタイプを含む)と関数名に基づいてコンパイル時に実際に呼び出す関数を決定します。
多形を達成するためには、関数署名は、オブジェクトポインタのタイプだけでは推定できません。すなわち、例では、p->sayHello() のラインコードは、実行時にpのタイプだけでは確認できません。呼び出しの関数はParent::sayHello ですか? Child:sayHelloですか?多形機構においては、各クラスの親およびサブクラスは、そのデータ構造において、複数のポインタを運ぶ必要があり、このポインタはこのクラスの虚数関数テーブルを指す。
クラスの虚数関数テーブル、すなわち、書き換え可能な関数のすべてのポインタテーブルは、オブジェクトの作成時に、その実際のタイプによって、その虚数関数のポインタが指す虚数関数のリストを決定します。上記の例では、Partent 1とChild類の虚数関数リストは、それぞれParent1::sayHello Child::sayHelloだけであり、コンパイラはコンパイル時に関数を呼び出して「参照虚数表のN番目の関数」と翻訳するような命令を出します。例えば、本例では「参照虚関数テーブルの最初の関数」と翻訳します。実行時に虚数表の本当の関数ポインタを読みます。実行時のCPUの価格は基本的に一次ポインタの参照と次の表へのアクセスです。
Part 1もChildオブジェクトもカスタムデータ構造がありません。以下のコードを実行すると、Part 1とChildオブジェクトの実際のデータ構造の大きさは8バイトであること、すなわち、虚関数リストポインタのみであることが確認できます。Part 1とChild 1のオブジェクトを64ビットの整数として出力すると、p 1、p 2の値が同じで、p 3は前とは違っています。この値は該当する種類の虚函数表の住所です。

Parent1* p1 = new Parent1();
Parent1* p2 = new Parent1();
Parent1* p3 = new Child();
printf("sizeof Parent1: %d, sizeof Child: %d
", sizeof(Parent1), sizeof(Child)); printf("val on p1: %lld
", *(int64_t*)p1); printf("val on p2: %lld
", *(int64_t*)p2); printf("val on p3: %lld
", *(int64_t*)p3);
C+++多態と多重継承
非常に興味深い問題があります。C++の多重継承が発生した時、どのように多状態をサポートしますか?先ほど述べたように、多形の原理はコンパイラがメンバー関数を「参照虚数関数テーブル中のN番目の関数」としてコンパイルし、虚数関数テーブルはオブジェクトデータ構造中の位置と虚数関数リストを呼び出すためのいくつかの関数をコンパイルするときに決定する必要があります。複数の継承オブジェクトが虚数関数のリストしかない場合、異なる親の虚数関数リストの位置が衝突します。複数の虚関数リストがある場合、コンパイルすると虚関数リストポインタのデータ構造内の位置が分かりにくくなります。C+++は、すべての親のデータ構造(ダミーポインタリストを含む)を対象のデータ構造上に順次並べ、そのオブジェクトのポインタがデータ構造の開始位置を正常に指しているという非常に巧妙な方法をとっている。ポインタがタイプ変換されると、C++コンパイラはポインタの値をできるだけ調整して、そのポインタの種類に対応する位置を指します。ポインタの値はこの過程で変化した。
例えば、Child類は、Part 1、Part 2の2つのクラスを継承している場合、ChildポインタがPart 1のポインタに変換された時、ポインタの値を調整しません。ただし、ChildをPart 2に変換するには、ポインタとは、Part 1のデータ構造長を増加させる値を指し、対応するPart 2のデータ構造の開始位置を指す必要がある。この例では、Part 1のデータ構造は虚数関数リストポインタのみで、64ビットのマシンでは長さ8であるため、ChildポインタがPart 2ポインタに変換されると、その値は8.

class Parent1 {
  public:
  virtual void sayHello() { printf("Hello from parent1!
"); } }; class Parent2 { public: virtual void sayHi() { printf("Hi from Parent2!
"); } }; class Child : public Parent1, public Parent2 { public: virtual void sayHello() { printf("Hello from child!
"); } virtual void sayHi() { printf("Hi from child!
"); } }; int main() { Child *p = new Child(); printf("size of Child: %d", sizeof(Child)); printf("pointer val as Child*: %lld
", int64_t(p)); printf("pointer val as Parent1*: %lld
", int64_t((Parent1*)p)); printf("pointer val as Parent2*: %lld
", int64_t((Parent2*)p)); }
このコードを実行すると、Childデータ構造のサイズは16、すなわち2つのポインタに増加することがわかった。そして、ポインタの値は、後の2つのタイプの変換時とは異なり、64ビットのマシンでは8バイト、つまり、Partent 1のデータ構造サイズが異なります。また、pをVoidポインタに変換してからPartentポインタに変換すると、コンパイラはこのオフセット量を正確に推定できなくなり、未定義挙動が発生します。
この特性は実は非常に興味深い事実を示しています。C++コンパイラはコンパイルするときに、ポインタのオフセットを推定することができます。コンパイラは、ポインタがオブジェクトに向けられている本当のタイプを推測することもできるはずです。では、コンパイルすることができますが、オブジェクトの本当のタイプを推論するためには、虚関数テーブルは何のために必要ですか?正しい関数を直接推論して呼び出したらいいじゃないですか?問題は、実際にコンパイル時に多形関数呼び出しを推定すると、異なるタイプのオブジェクトに異なるバイナリコードを生成することを意味する。同じ行のコードは、ポインタの値によって発生する関数呼び出しが異なります。このようにしても、サードパーティライブラリは、テンプレートライブラリのような関連する推論を行うためにソースコードを提供する必要があることを意味する。これはすべて受け入れられないので、虚数関数のリストは依然として必要です。虚数関数のリストによって、ポインタのコードを使って一致するマシンコードを生成することができます。
別の観点から理解すると、コンパイラは完全なAppをコンパイルする時に、すべての変数の本当のタイプを推測することができますが、これは多すぎるコンテキストに連絡する必要があります。セグメントコードをコンパイルするには、このコード入力パラメータのタイプ以外のコンテキスト情報が必要であり、コンテキスト情報によって異なるバイナリファイルが生成されます。これは許容できません。
Java多形比較
Javaの多状態機構はC++より簡単なので,理論的にはC+++の機構を用いてJava多状態を実現できる。しかし、C++はJavaとは決定的な違いがあります。C++は親の方法を要求しています。Virtualキーワードを修飾する時に書き換えられます。これは、コンパイラが親のクラスをコンパイルするときに、それらの関数が書き換えられていることを確認することができ、書き換え不可能な関数を直接コンパイルするときに呼び出しの具体的な関数を決定することができ、書き換え可能な関数に対しては、虚ポインタ表を使って処理することができるということです。Javaの方法はデフォルトでは書き換えることができますので、Javaメソッドの呼び出しはすべて仮想関数リストを検索するプロセスを経て、C++を書き換えない関数よりも多くのオーバーヘッドが必要だと考えられます。
Javaは多重継承をサポートしていませんが、JavaはインタフェースInterfaceをサポートしています。インタフェースは複数の継承と似ているところがあります。簡単に虚関数表を使って検索することはできません。クラスは、それぞれのInterfaceのために虚関数リストを生成する必要があります。C++の場合と似ています。OpenJDK文書による指摘、クラス定義でInterfaceの虚数関数リストを見つける方法は乱暴である:クラス実現のすべてのInterfaceリストを巡回検索する。文書では、真の多重継承はまれであり、通常は単一継承に帰結できると指摘しています。これに対してプロセスを経ると、様々な最適化があるかもしれません。筆者は深く理解していません。
JavaとC++の違いを考えると、C++が実行されていない時のタイプがコンパイラによってコンパイルされた時に、ポインタが指す位置にオブジェクトが正確なデータ構造があることを保証します。サブクラスのポインタを親クラスのポインタ変数に割り当てる場合、コンパイラは極力調整しますが、Voidポインタの割り当てなどが発生した場合、コンパイラはポインタが指す位置に正確なオブジェクトデータ構造があるとは保証できません。このステップは文法上に誤りがない限り、すぐにエラーを報告しません。コンパイラも問題が発生するかどうかを確認できません。必ずそのポインタが実際に参照を解くなど異常が発生してからエラーが発生します。Javaには実行時タイプがあり、対象を異なるタイプの変数に割り当てた場合、実行時にタイプチェックを行います。正確なタイプの継承関係がない場合は、フォートタイムズでエラーが発生します。
また、JavaのInterfaceとC++の多重継承を比較すると、Interfaceの運転時の時間オーバーヘッドはC++の多重継承よりもずっと大きいことが分かります。しかし、C+++複数の継承は、各親のためのポインタを追加する必要があり、コンパイラはコンパイルする時に、より多くの作業を完了する必要があります。JavaはC++より「強いタイプ」の言語です。
ここで、C++の多态とJavaの违いを実现するための多重継承についての文章をここに绍介します。C++の多态と复数の継承内容については、以前の文章を検索したり、以下の関连する文章を引き続きご覧ください。これからもよろしくお愿いします。