仮想関数

C++C++ は、デフォルトによりコンパイル時に、関数呼び出しを正しい関数定義とマッチングさせます。 これは静的バインディング と呼ばれます。

コンパイラーに、関数呼び出しと正しい関数定義を、実行時にマッチングさせることを指定できます。 これは、動的バインディング と呼ばれます。

特定の関数に対して、コンパイラーに動的バインディングを使用させたい場合、 キーワード virtual を指定して関数を宣言します。

次の例は、静的バインディングと動的バインディングの違いを示しています。 最初の例は、静的バインディングを示しています。

#include <iostream>
using namespace std;
 
struct A {
   void f() { cout << "Class A" << endl; }
};
 
struct B: A {
   void f() { cout << "Class B" << endl; }
};
 
void g(A& arg) {
   arg.f();
}
 
int main() {
   B x;
   g(x);
}

次に、上記の例の出力を示します。

Class A

関数 g() が呼び出されると、引き数は、型 B のオブジェクトを参照しますが、 関数 A::f() がコールされます。 コンパイル時にコンパイラーが唯一認知できるのは、関数 g() の引き数が、A から派生したオブジェクトの参照である可能性があるということです。 引き数が、型 A のオブジェクトへの参照なのか、 または型 B のオブジェクトへのものなのかどうかを判別することはできません。 しかし、これは実行時に判別されます。 次の例は、A::f()virtual キーワードで宣言されている点を除いては、 直前の例と同じです。

#include <iostream>
using namespace std;
 
struct A {
   virtual void f() { cout << "Class A" << endl; }
};
 
struct B: A {
   void f() { cout << "Class B" << endl; }
};
 
void g(A& arg) {
   arg.f();
}
 
int main() {
   B x;
   g(x);
}

次に、上記の例の出力を示します。

Class B

virtual キーワードは、参照の型ではなく、 参照先のオブジェクト型を使用して、f() のための適切な定義を選択する必要があることを コンパイラーに指示します。

したがって、仮想関数 とは、 別の派生クラスのために再定義できるメンバー関数です。 また、たとえオブジェクトの基底クラスに対するポインター、または参照を使用して関数を呼び出したとしても、 対応する派生クラスのオブジェクト向けに再定義された仮想関数を、 コンパイラーが呼び出せることも保証できます。

仮想関数を宣言するクラス、または継承するクラスは、ポリモアフィック と呼ばれます。

仮想メンバー関数は、任意の派生クラスにおいて、どのメンバー関数とも同じように、再定義することができます。 クラス Af という名前の仮想関数を宣言し、A から直接的、 または間接的に B という名前のクラスを派生させたとします。 A::f と同じ名前、および同じパラメーター・リストを指定して、 クラス Bf という名前の関数を宣言する場合、B::f もまた仮想であり (virtual キーワードを使用して B::f を宣言しているかどうかには関係なく)、 それは A::fオーバーライド します。 しかし、A::fB::f のパラメーター・リストが異なり、A::fB::f は違うものであると見なされる場合、B::fA::f をオーバーライドしませんし、B::f は、仮想ではありません (virtual キーワードを使用してこれを宣言していない場合)。 代わりに、B::f は、A::f隠します。 次の例は、このことを示しています。

#include <iostream>
using namespace std;
 
struct A {
   virtual void f() { cout << "Class A" << endl; }
};
 
struct B: A {
   void f(int) { cout << "Class B" << endl; }
};
 
struct C: B {
   void f() { cout << "Class C" << endl; }
};
 
int main() {
   B b; C c;
   A* pa1 = &b;
   A* pa2 = &c;
//   b.f();
   pa1->f();
   pa2->f();
}

次に、上記の例の出力を示します。

Class A
Class C

関数 B::f は、仮想ではありません。 これは A::f を隠します。 つまり、コンパイラーは、関数呼び出し b.f() を許可しません。 関数 C::f は、仮想です。A::fC で可視ではありませんが、 これは A::f をオーバーライドします。

基底クラス・デストラクターを仮想として宣言する場合、 派生クラス・デストラクターは、デストラクターが継承されていなくても、 基底クラス・デストラクターをオーバーライドします。

オーバーライドする仮想関数の戻りの型は、オーバーライドされる仮想関数の戻りの型とは異なる場合があります。 このオーバーライド関数は、共変仮想関数 と呼ばれます。

B::f が、仮想関数 A::f をオーバーライドするとします。 A::fB::f の戻りの型は、 次の条件のすべてが満たされるかどうかで変わります。

次の例は、このことを示しています。

#include <iostream>
using namespace std;
 
struct A { };
 
class B : private A {
   friend class D;
   friend class F;
};
 
A global_A;
B global_B;
 
struct C {
   virtual A* f() {
      cout << "A* C::f()" << endl;
      return &global_A;
   }
};
 
struct D : C {
   B* f() {
      cout << "B* D::f()" << endl;
      return &global_B;
   }
};
 
struct E;
 
struct F : C {
 
//   Error:
//   E is incomplete
//   E* f();
};
 
struct G : C {
 
//   Error:
//   A is an inaccessible base class of B
//   B* f();
};
 
int main() {
   D d;
   C* cp = &d;
   D* dp = &d;
 
   A* ap = cp->f();
   B* bp = dp->f();
};

次に、上記の例の出力を示します。

B* D::f()
B* D::f()

ステートメント A* ap = cp->f() は、D::f() を呼び出して、 戻されるポインターを型 A* に変換します。 ステートメント B* bp = dp->f() は、D::f() も呼び出しますが、 戻されるポインターを変換しません。戻り型は B* となります。 コンパイラーは、E が完全なクラスではないので、 仮想関数 F::f() の宣言を許可しません。 コンパイラーは、クラス AB にアクセス可能な基底クラスではないので、 仮想関数 G::f() の宣言を許可しません (フレンド・クラス D および F と異なり、B の定義は、クラス G のメンバーにアクセス権を与えません)。

定義によると、仮想関数は、基底クラスのメンバー関数であり、特定のオブジェクトに従って、 関数のどのインプリメンテーションを呼び出すかを決めるので、 仮想関数をグローバルにも静的にもすることはできません。 仮想関数を別のクラスのフレンドとして宣言することができます。

基底クラスにおいて仮想と宣言した関数の場合でも、 スコープ・レゾリューション (::) 演算子を使用すれば、それを直接にアクセスすることができます。 この場合、仮想関数呼び出しのメカニズムを抑止し、 基底クラスで定義された関数インプリメンテーションが使用されます。 さらに、派生クラスで仮想メンバー関数を再オーバーライドしなければ、 その関数に対する呼び出しでは、基底クラスで定義された関数インプリメンテーションが使用されます。

仮想関数は次のいずれかでなければなりません。

1 つまたは複数の純粋仮想メンバー関数を含む基底クラスは、抽象クラス と呼ばれます。

関連参照

IBM Copyright 2003