C++ ポリモーフィズム

ポリモーフィズム文字どおり、複数の形態を意味します。

クラス間に階層構造が存在し、クラス同士が継承によって関連付けられている場合に、ポリモーフィズムが使用されます。

C++ では、ポリモーフィズム(Polymorphism)はオブジェクト指向プログラミングの重要な特性の1つです。

C++ のポリモーフィズムでは、基本クラスのポインタまたは参照を使用して派生クラスのオーバーライド済みメソッドを呼び出せるため、同じインターフェースで異なる動作を表現できます。

ポリモーフィズムによりコードはより柔軟で汎用的になり、プログラムは基本クラスのポインタや参照を使って異なる型のオブジェクトを操作でき、オブジェクトの型を明示的に区別する必要がありません。これによりコードの拡張性が高まり、新しい形状クラスを追加するときにメインプログラムを変更する必要がありません。

以下はポリモーフィズムの重要なポイントです:

仮想関数(Virtual Functions):

  • 基本クラスで関数を仮想関数として宣言するには、キーワードvirtual。
  • 派生クラスはこの仮想関数をオーバーライド(override)できます。
  • 仮想関数を呼び出すとき、オブジェクトの実際の型に基づいて、どのバージョンの関数を呼び出すかが決まります。

動的バインディング(Dynamic Binding):

  • 遅延バインディング(Late Binding)とも呼ばれ、実行時に関数呼び出しの具体的な実装を決定します。
  • 基本クラスを指すポインタまたは参照を使用して仮想関数を呼び出す必要があります。コンパイラは実行時にオブジェクトの実際の型に基づいて、どの関数を呼び出すかを決定します。

純粋仮想関数(Pure Virtual Functions):

  • 純粋仮想関数を含むクラスは抽象クラス(Abstract Class)と呼ばれ、直接インスタンス化することはできません。
  • 純粋仮想関数には関数本体がなく、宣言時には= 0。
  • 派生クラスに具体的な実装を提供することを強制します。

ポリモーフィズムの実装メカニズム:

  • 仮想関数テーブル(V-Table):C++ の実行時は仮想関数テーブルを使用してポリモーフィズムを実現します。仮想関数を含むすべてのクラスには仮想関数テーブルがあり、そのテーブルにはクラス内のすべての仮想関数へのポインタが格納されます。
  • 仮想関数ポインタ(V-Ptr):オブジェクトには、そのクラスの仮想関数テーブルを指すポインタが含まれます。

ポリモーフィズムを使用する利点:

  • コードの再利用:基本クラスのポインタまたは参照を使用して、異なる型の派生クラスオブジェクトを操作でき、コードの再利用を実現します。
  • 拡張性:新しい派生クラスを追加するとき、基本クラスに依存するコードを変更する必要はなく、新しいクラスが仮想関数を正しくオーバーライドしていることだけを確認すればよいです。
  • 疎結合化:ポリモーフィズムにより、プログラム設計をよりモジュール化し、クラス間の結合度を低減できます。

注意事項:

  • 基本クラスのポインタまたは参照を介して仮想関数を呼び出した場合にのみ、ポリモーフィズムが発生します。
  • 派生クラスのオブジェクトを直接使用して関数を呼び出す場合、呼び出されるのは基本クラスのバージョンではなく、派生クラスのバージョンです。
  • ポリモーフィズムには実行時型情報(RTTI)が必要であり、プログラムのオーバーヘッドが増加する可能性があります。

例 1

簡単な例を通じて、ポリモーフィズムの応用を理解しましょう:

例 1

#include <iostream>
using namespace std;

// 基本クラス Animal
class Animal {
public:
    // 仮想関数 sound。さまざまな動物の発声のためのインターフェースを提供
    virtual void sound() const {
        cout << "Animal makes a sound" << endl;
    }
   
    // 仮想デストラクタにより、派生クラスオブジェクトが正しく破棄されることを保証
    virtual ~Animal() {
        cout << "Animal destroyed" << endl;
    }
};

// 派生クラス Dog。Animal から継承
class Dog : public Animal {
public:
    // sound メソッドをオーバーライド
    void sound() const override {
        cout << "Dog barks" << endl;
    }
   
    ~Dog() {
        cout << "Dog destroyed" << endl;
    }
};

// 派生クラス Cat。Animal から継承
class Cat : public Animal {
public:
    // sound メソッドをオーバーライド
    void sound() const override {
        cout << "Cat meows" << endl;
    }
   
    ~Cat() {
        cout << "Cat destroyed" << endl;
    }
};

// ポリモーフィズムのテスト
int main() {
    Animal* animalPtr;  // 基本クラスポインタ

    // Dog オブジェクトを作成し、Animal ポインタに割り当て
    animalPtr = new Dog();
    animalPtr->sound();  // Dog の sound メソッドを呼び出す
    delete animalPtr;    // メモリを解放し、Dog と Animal のデストラクタを呼び出す

    // Cat オブジェクトを作成し、Animal ポインタに割り当て
    animalPtr = new Cat();
    animalPtr->sound();  // Cat の sound メソッドを呼び出す
    delete animalPtr;    // メモリを解放し、Cat と Animal のデストラクタを呼び出す

    return 0;
}

プログラムの実行結果は次のとおりです:

Dog barks
Dog destroyed
Animal destroyed
Cat meows
Cat destroyed
Animal destroyed

コードの説明

基本クラスAnimal:

  • Animalクラスには仮想関数が定義されていますsound()、これは仮想関数(virtual)であり、動物の発声動作を表します。
  • ~Animal()は仮想デストラクタであり、基本クラスポインタが指す派生クラスオブジェクトを解放する際、派生クラスのデストラクタを正しく呼び出してメモリリークを防ぎます。

派生クラスDogとCat:

  • DogとCatクラスはすべてAnimalクラスから派生し、それぞれsound()メソッドを実装しています。
  • Dogのsound()"Dog barks" と出力します;Catのsound()"Cat meows" と出力します。これにより、同じメソッド(sound())が異なるクラスで異なる動作を示します。

main関数main():

  • 基本クラスポインタを作成しますanimalPtr。
  • を使用してnew Dog()作成しDogオブジェクトを生成し、そのアドレスをanimalPtrに代入します。このとき、animalPtr->sound()を呼び出すと "Dog barks" と出力されます。なぜならanimalPtrが実際に指しているのはDogオブジェクトだからです。
  • 解放Dogオブジェクトのときは、最初に呼び出しますDogのデストラクタを呼び出し、次にAnimalのデストラクタを呼び出します。
  • 使用してnew Cat()作成しCatオブジェクトを生成してanimalPtrに代入し、次に呼び出しますanimalPtr->sound()"Cat meows" を出力し、ポリモーフィズムの動作を示します。

重要な概念

  • 仮想関数:基本クラスでvirtualキーワードを使用して仮想関数を宣言すると、派生クラスはこの関数をオーバーライドでき、実行時にオブジェクトの型に応じて正しい関数を呼び出せます。

  • 動的バインディング:C++ のポリモーフィズムは動的バインディングによって実現されます。実行時に、基本クラスポインタanimalPtrは、それが実際に指すオブジェクトの型(Dog 或 Cat)に応じて対応するsound()メソッドを呼び出します。

  • 仮想デストラクタ:ポリモーフィズムの動作を持つ基本クラスでは、デストラクタはvirtualとして宣言する必要があります。これにより、派生クラスオブジェクトを削除する際に派生クラスのデストラクタが呼び出され、リソースリークを防ぎます。


例 2

次の例では、ポリモーフィズムを使用して、汎用の Shape 基本クラスと、Rectangle および Triangle という2つの派生クラスを実装します。

基本クラスポインタを介して異なる派生クラスのメソッドを呼び出し、ポリモーフィズムの動的バインディング特性を示しています。

例 2

#include <iostream> using namespace std; // 基本クラス Shape。形状を表す class Shape { protected: int width, height; // 幅と高さ public: // コンストラクタ。デフォルト引数付き Shape(int a = 0, int b = 0) : width(a), height(b) { } // 仮想関数 area。面積を計算する // virtual キーワードを使用して、ポリモーフィズムを実現 virtual int area() { cout << "Shape class area: " << endl; return 0; } }; // 派生クラス Rectangle。長方形を表す class Rectangle : public Shape { public: // コンストラクタ。基本クラスのコンストラクタを使用して width と height を初期化 Rectangle(int a = 0, int b = 0) : Shape(a, b) { } // area 関数をオーバーライドし、長方形の面積を計算 int area() override { cout << "Rectangle class area: " << endl; return width * height; } }; // 派生クラス Triangle。三角形を表す class Triangle : public Shape { public: // コンストラクタ。基本クラスのコンストラクタを使用して width と height を初期化 Triangle(int a = 0, int b = 0) : Shape(a, b) { } // area 関数をオーバーライドし、三角形の面積を計算 int area() override { cout << "Triangle class area: " << endl; return (width * height / 2); } }; // main関数 int main() { Shape *shape; // 基本クラスポインタ Rectangle rec(10, 7); // 長方形オブジェクト Triangle tri(10, 5); // 三角形オブジェクト // 基本クラスポインタを長方形オブジェクトに指し、area 関数を呼び出す shape = &rec; cout << "Rectangle Area: " << shape->area() << endl; // 基本クラスポインタを三角形オブジェクトに指し、area 関数を呼び出す shape = &tri; cout << "Triangle Area: " << shape->area() << endl; return 0; }

上記のコードをコンパイルして実行すると、次の結果が得られます:

Rectangle Area: Rectangle class area: 
70
Triangle Area: Triangle class area: 
25

コード分析

Shape クラスの定義:

  • Shapeは抽象基本クラスであり、仮想関数を定義しています。area()。area()は面積を計算する仮想関数であり、virtualキーワードを使用しています。これにより、派生クラスでこの関数をオーバーライドしてポリモーフィズムを実現できます。
  • widthとheightであるprotected属性は、Shapeクラスおよびその派生クラス内でのみアクセスできます。
// 基类 Shape,表示形状
class Shape {
   protected:
      int width, height; // 宽度和高度

   public:
      // 构造函数,带有默认参数
      Shape(int a = 0, int b = 0) : width(a), height(b) { }

      // 虚函数 area,用于计算面积
      virtual int area() {
         cout << "Shape class area: " << endl;
         return 0;
      }
};

Rectangle クラスの定義:

  • RectangleはShapeクラスを継承し、area()メソッドをオーバーライドして、長方形の面積を計算します。
  • area()メソッドはoverrideキーワードを使用しており、これは基本クラスのShapeのarea()メソッドのオーバーライドであることを示します。
  • Rectangle::area()を返します。width * height、つまり長方形の面積です。
// 派生类 Rectangle,表示矩形
class Rectangle : public Shape {
   public:
      // 构造函数,使用基类构造函数初始化 width 和 height
      Rectangle(int a = 0, int b = 0) : Shape(a, b) { }

      // 重写 area 函数,计算矩形面积
      int area() override { 
         cout << "Rectangle class area: " << endl;
         return width * height;
      }
};

Triangle クラスの定義:

  • Triangleクラスも継承しShape、そしてオーバーライドしたarea()メソッドで、三角形の面積を計算します。
  • Triangle::area()返しますwidth * height / 2、これは三角形の面積の公式です。
// 派生类 Triangle,表示三角形
class Triangle : public Shape {
   public:
      // 构造函数,使用基类构造函数初始化 width 和 height
      Triangle(int a = 0, int b = 0) : Shape(a, b) { }

      // 重写 area 函数,计算三角形面积
      int area() override { 
         cout << "Triangle class area: " << endl;
         return (width * height / 2); 
      }
};

メイン関数における多態性の動作:

  • 基底クラスポインタを定義しましたshape、このポインタは任意のShapeクラスのオブジェクトまたはその派生クラスのオブジェクトを指すことができます。
  • まず、shapeポインタをRectangleオブジェクトに向けますrec、次にshape->area()を呼び出します。なぜならarea()は仮想関数であり、このとき動的にRectangle::area()にバインドされ、長方形の面積を出力します。
  • 続いて、shapeポインタをTriangleオブジェクトに向けますtri、shape->area()を呼び出すときは動的にTriangle::area()にバインドされ、三角形の面積を出力します。
// 主函数
int main() {
   Shape *shape;           // 基类指针
   Rectangle rec(10, 7);   // 矩形对象
   Triangle tri(10, 5);    // 三角形对象

   // 将基类指针指向矩形对象,并调用 area 函数
   shape = &rec;
   cout << "Rectangle Area: " << shape->area() << endl;

   // 将基类指针指向三角形对象,并调用 area 函数
   shape = &tri;
   cout << "Triangle Area: " << shape->area() << endl;

   return 0;
}

重要な概念

  • 仮想関数:基底クラスでShapeに仮想関数が定義されていますarea()。仮想関数の役割は、派生クラスがこの関数をオーバーライドできるようにし、実行時にポインタの実際のオブジェクト型に基づいて適切な関数実装を呼び出すことです。

  • 動的バインド:なぜならarea()は仮想関数であり、shape->area()呼び出し時には実行時にshape実際に指しているオブジェクトの型(Rectangle 或 Triangle)に応じて対応するarea()実装を呼び出します。実行時にどの関数を呼び出すかを決定するこのメカニズムは動的バインドと呼ばれ、多態性の核です。

  • 基底クラスポインタの多態性:基底クラスポインタshapeは、任意のShapeから派生したオブジェクトを指すことができます。...ときshapeが異なる派生クラスオブジェクトを指すとき、shape->area()を呼び出すと異なる動作が生じ、これは多態性の特性を示しています。

  • 仮想関数

    仮想関数は基底クラスでキーワードvirtualを使用して宣言される関数です。

    仮想関数はサブクラスがそれをオーバーライドすることを許可し、実行時に基底クラスのポインタまたは参照を通じてサブクラスのオーバーライド版を呼び出し、動的バインドを実現します。

    私たちが望むのは、プログラムの任意の時点で、呼び出されるオブジェクトの型に応じて呼び出す関数を選択できることであり、この操作は動的リンク、または後期バインドと呼ばれます。

    特徴:

    • 基底クラスで実装を持つことができる。通常、仮想関数は基底クラスでデフォルト実装を提供しますが、サブクラスはオーバーライドを選択できます。
    • 動的バインド:実行時にオブジェクトの実際の型に応じて対応する関数バージョンを呼び出します。
    • オプションのオーバーライド:派生クラスは仮想関数を選択的にオーバーライドできますが、必須ではありません。

    例

    #include <iostream>
    using namespace std;

    class Animal {
    public:
        virtual void sound() {  // 仮想関数
            cout << "Animal makes a sound" << endl;
        }
    };

    class Dog : public Animal {
    public:
        void sound() override {  // 仮想関数のオーバーライド
            cout << "Dog barks" << endl;
        }
    };

    int main() {
        Animal *animal = new Dog();
        animal->sound();  // 出力: Dog barks
        delete animal;
    }

    上記のコードでは、sound は Animal クラスの仮想関数です。Animal* ポインタ animal を介して sound() を呼び出すと、プログラムは実際のオブジェクト型(Dog)に基づいて Dog::sound() を呼び出します。

    純粋仮想関数

    基底クラスで仮想関数を定義したいが、派生クラスでその関数を再定義してオブジェクトにより適したものにしたい、しかし基底クラスでは仮想関数に意味のある実装を与えられない場合、このようなときに純粋仮想関数を使用します。

    純粋仮想関数は実装を持たない仮想関数であり、基底クラスで= 0を使用して宣言されます。

    純粋仮想関数は、基底クラスがインターフェースを定義するが、具体的な実装は派生クラスが担当することを示します。

    純粋仮想関数により基底クラスは抽象クラス(abstract class)となり、インスタンス化できません。

    特徴:

    • 基底クラスで= 0として宣言する必要があります。実装がないことを示し、サブクラスはオーバーライドする必要があります。
    • 抽象クラス:純粋仮想関数を含むクラスは直接インスタンス化できず、派生クラスがすべての純粋仮想関数を実装して初めてオブジェクトを作成できます。
    • インターフェース定義:純粋仮想関数は通常、インターフェースを定義し、派生クラスに具体的な動作を実装させるために使用されます。

    基底クラスの仮想関数 area() を次のように書き換えることができます:

    #include <iostream> using namespace std; class Shape { public: virtual int area() = 0; // 純粋仮想関数、サブクラスにこのメソッドの実装を強制 }; class Rectangle : public Shape { private: int width, height; public: Rectangle(int w, int h) : width(w), height(h) { } int area() override { // 純粋仮想関数の実装 return width * height; } }; int main() { Shape *shape = new Rectangle(10, 5); cout << "Rectangle Area: " << shape->area() << endl; // 出力: Rectangle Area: 50 delete shape; }

    = 0コンパイラに関数に本体がないことを伝え、上記の仮想関数は純粋仮想関数です。。

    仮想関数と純粋仮想関数の比較

    特性仮想関数(Virtual Function)純粋仮想関数(Pure Virtual Function)
    定義基底クラスでvirtualを使用して宣言し、実装がある基底クラスで= 0を使用して宣言し、実装がない
    サブクラスのオーバーライドサブクラスはオーバーライドを選択できるサブクラスは実装する必要がある
    抽象性クラスをインスタンス化できるクラスを抽象クラスにし、インスタンス化できない
    用途デフォルトの動作を提供し、サブクラスのオーバーライドを許可するインターフェースを定義し、サブクラスに具体的な動作の実装を強制する
    その他の拡張