C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++の非仮想関数の内部から仮想関数を呼び出すとどうなる?

C++の継承と多態性(ポリモーフィズム)には興味深い挙動がいくつかあります。本記事では「仮想関数を非仮想関数の内部から呼び出すとどうなるのか」というテーマについて、まず2つのケースを実際のコードで確認し、その後で内部的な仕組みを分析します。

  • ケース1:仮想関数を一切使わずにプログラムを実行する

  • ケース2:非仮想関数の内部で仮想関数を呼び出してプログラムを実行する

ケース1:仮想関数を使わない場合

まずは、virtualキーワードを使用しない次の実装例を見てみましょう。

#include <iostream>
using namespace std;

class BaseClass {
public:
   void display(){
      cout << "Print function from the base class" << endl;
   }
   void call_disp(){
      cout << "Calling display() from base class" << endl;
      this->display();
   }
};

class DerivedClass : public BaseClass {
public:
   void display() {
      cout << "Print function from the derived class" << endl;
   }
};

int main() {
   BaseClass *bp = new DerivedClass;
   bp->call_disp();
}

出力結果

Calling display() from base class
Print function from the base class

ポインタbpはDerivedClassのインスタンスを指していますが、呼び出されたのは基本クラス側の関数です。これは、display()もcall_disp()も仮想関数ではないため、コンパイル時に静的結合(スタティックバインディング)で呼び出し先が確定するからです。つまり、ポインタの「静的な型」(ここではBaseClass*)に基づいて関数が決定されます。

ケース2:非仮想関数の内部から仮想関数を呼び出す場合

次に、display()にvirtualを付けたコードを見てみましょう。ポイントは、call_disp()自体は非仮想関数のままであることです。

#include <iostream>
using namespace std;

class BaseClass {
public:
   virtual void display(){
      cout << "Print function from the base class" << endl;
   }
   void call_disp(){
      cout << "Calling display() from base class" << endl;
      this->display();
   }
};

class DerivedClass : public BaseClass {
public:
   void display() override {
      cout << "Print function from the derived class" << endl;
   }
};

int main() {
   BaseClass *bp = new DerivedClass;
   bp->call_disp();
}

出力結果

Calling display() from base class
Print function from the derived class

今度は、基本クラスの非仮想関数call_disp()の内部から呼び出されたにもかかわらず、派生クラス側のdisplay()が実行されました。この結果から、仮想関数が非仮想関数の内部から呼び出された場合でも、多態性は正しく機能することが分かります。どの関数が呼ばれるかはコンパイル時ではなく実行時に決定され、その仕組みを支えているのがvptrとvtableです。

vtableとvptrとは

  • vtable(仮想関数テーブル) − クラスごとに保持される、仮想関数へのポインタのテーブルです。

  • vptr(仮想テーブルポインタ) − オブジェクトのインスタンスごとに保持され、自分のクラスのvtableを指すポインタです。

呼び出し時には、オブジェクトが持つvptrを通じて対応するvtableが参照され、実際のオブジェクト型に応じた関数が動的に選択されます。この仕組みにより、呼び出し経路が非仮想関数の内部であっても、仮想関数なら実行時の多態的なディスパッチが行われるのです。

まとめ

仮想関数を使わない場合は静的結合により基本クラスの関数が呼ばれますが、一度virtualと宣言すれば、たとえ非仮想関数の内部から呼び出される場合でも、実行時の動的結合によって適切な派生クラスの関数が呼び出されます。C++の多態性は「呼び出し元の関数が仮想かどうか」ではなく、「呼び出される関数自身が仮想かどうか」で決まるという点を押さえておきましょう。

  1. C++のアロー演算子(矢印演算子)とは?ドット演算子との違いをわかりやすく解説

    C++には、クラスのメンバーにアクセスするための演算子として「ドット演算子(.)」と「アロー演算子(->)」の2種類があります。どちらも同じ目的で使われますが、使用する場面が異なります。C++では、class、struct、unionとして宣言された型はすべて「クラスタイプ」とみなされます。以下の説明は、これらすべてに共通して当てはまります。ドット演算子とアロー演算子の基本的な違いa.b は、b がオブジェクトa(またはオブジェクトへの参照)のメンバーである場合にのみ使用できます。つまり、a.b の形式では、a は必ず実際のオブジェクト(またはオブジェクトへの参照)になります。a->

  2. C++のドット演算子(.)とは?矢印演算子との違いと使い分けを解説

    C++では、クラスのメンバーにアクセスするために「ドット演算子(.)」と「矢印演算子(->)」の2種類が用意されています。どちらも同じ目的で使われますが、使用する場面が異なります。C++において、class・struct・unionとして宣言された型はすべて「クラス型」とみなされます。以下では、これらに対する両演算子の使い方を説明します。ドット演算子(a.b)a.b は、b がオブジェクト a(またはオブジェクトへの参照[1])のメンバーである場合にのみ使用できます。つまり、a.b の形式では、a は必ずクラス型の実際のオブジェクト(またはその参照)である必要があります。矢印演算子(a-