C++の仮想関数とその静的タイプと動的タイプの解析

3213 ワード

仮想関数はC++言語が導入した重要な特性であり、「動的バインド」メカニズムを提供し、継承の意味を比較的明確にする.(1)ベースクラスは汎用的なデータおよび操作を抽象化しており、データについては、そのデータメンバーが各派生クラスで使用される必要がある場合、ベースクラスに宣言する必要がある.操作については、意味が変更または拡張されるかどうかにかかわらず、各派生クラスに意味がある場合は、ベースクラスに宣言する必要があります.(2)派生クラスごとに意味が完全に一致し、変更や拡張を必要としない場合、これらの操作はベースクラスの非仮想メンバー関数として宣言される操作がある.各派生クラスは、ベースクラスの派生クラスとして宣言されると、デフォルトではこれらの非仮想メンバー関数の宣言/実装が継承され、デフォルトでベースクラスを継承するデータメンバーと同様に、別途宣言する必要がなく、継承によるコード再利用の利点です.(3)また,派生クラスごとに意味があるが,その意味は異なる操作もある.この場合、これらの操作はベースクラスの仮想メンバー関数として宣言する必要があります.各派生クラスは、これらの仮想メンバー関数の宣言/実装もデフォルトで継承されますが、意味的には、これらの仮想メンバー関数の実装を変更または拡張する必要があります.また、これらの変更または拡張を実装する際に、派生クラス固有の追加のデータを使用する必要がある場合は、これらのデータを派生クラス独自のデータメンバーとして宣言します.さらに、より大きな背景にある継承アーキテクチャを考慮すると、より高い階層のプログラムフレームワーク(継承アーキテクチャの使用者)がこの継承アーキテクチャを使用する場合、抽象階層のオブジェクトセット(すなわちベースクラス)を処理します.このオブジェクトセットのメンバーは実質的に様々な派生クラスオブジェクトである可能性がありますが、このオブジェクトセット内のオブジェクトを処理する際に抽象階層の操作を使用します.これらの操作では、どの操作が各派生クラスにとって変わらないのか、どの操作が各派生クラスにとって異なるのかは区別されません.これは、実行時に実際に各操作が実行されると、実行時にシステムが「動的バインド」を使用する必要がある操作を認識し、この派生クラスに対応する修正または拡張された操作バージョンを見つけることができるためです.つまり、自分の問題ドメインのビジネスロジックに関心を持つだけで、正しいことを保証すれば、そのタスクは完了します.継承アーキテクチャ内に派生クラスが追加されたり、派生クラスが削除されたり、派生クラスの仮想関数の実装が変更されたりしても、コードを変更する必要はありません.これは,プログラムのモジュール化の程度が大幅に向上したことを意味する.モジュール化の向上は、拡張性、メンテナンス性、コードの可読性の向上を意味し、これも「オブジェクト向け」プログラミングの大きな利点です.
仮想関数の静的タイプと動的タイプは、サブクラスが再ロードされた仮想関数がpriveteである場合、親クラスのポインタでアクセスできますか?

#include  
class B 
{ 
public: 
  virtual void fun()  
  { 
    std::cout << "base fun called"; 
  }; 
}; 
class D : public B  
{ 
private: 
  virtual void fun()  
  { 
    std::cout << "driver fun called"; 
  }; 
}; 
int main(int argc, char* argv[]) 
{   
  B* p = new D(); 
  p->fun(); 
  return 0; 
} 

実行時に出力

driver fun called

この実験から、仮想関数のコンパイル時のいくつかの特徴をより深く理解することができます.
仮想関数呼び出しをコンパイルするとき、例えばp->fun()静的タイプで処理されるだけで、ここでpのタイプはBであり、実際に指向されるタイプ(ダイナミックタイプ)は考慮されない.
すなわち、p->fun();コンパイラはBを呼び出すfunとして対応するチェックと処理を行う.
Bでfunはpublicなので、ここでは「アクセス制御チェック」という関門で完全にパスできます.すると(*p->vptr[1])(p)のように処理され、pが実際に指す動的タイプはDであるため、pがパラメータとしてfunに渡されると(クラスの非静的メンバー関数はいずれもポインタパラメータをコンパイルし、その関数を呼び出すオブジェクトを指し、私たちが普段使っているthisがそのポインタの値である)、実際の実行時にp->vptr[1]はD::fun()のアドレスを取得し、つまり、この関数が呼び出され、これが動的に実行されるメカニズムです.
さらなる実験のために、Bのfunをprivateに、Dのfunをpublicに変更すると、コンパイルが間違ってしまいます.
C++の注意条項に「継承されたデフォルトパラメータ値を再定義しない」(Effective C++Item 37,never redefine a function's inherited default parameter value)があるのも同様です.もう一つ実験してもいいです.

class B 
{ 
public: 
  virtual void fun(int i = 1)  
  { 
    std::cout << "base fun called, " << i; 
  }; 
}; 
class D : public B  
{ 
private: 
  virtual void fun(int i = 2)  
  { 
    std::cout << "driver fun called, " << i; 
  }; 
}; 

実行すると出力されます

driver fun called, 1

この点、Effectiveでは「virtual関数系は動的にバインドされているが、デフォルトパラメータは静的にバインドされている」ということがよく知られている.つまり、コンパイル時にすでにpの静的タイプでデフォルトパラメータが処理されており、(*p->vptr[1])(p,1)のような形に変換されている.