見出し画像

C++ 再入門 その19 - 仮想関数と抽象クラス

前回のC++再入門では、親から継承したクラスを使う時に、子クラスとしての振る舞いだけではなく、親クラスに型変換することで、親クラスとしての振る舞いも出来ることを確認しました。

C++ 再入門 その18 - 継承と親子間の型キャスト

親クラスから継承したクラスに同じ振る舞いをさせたければ、それは親クラスのメソッドなりプロパティとして実装すれば良く、子クラス固有の処理を追加したければ、同じプロトタイプ(名前と引き数が同じ)で子クラスに書けば、子クラスのインスタンスからはそちらが呼ばれることもわかりました。この方法は子クラスとしてのインスタンスからしか呼び出すことが出来ません。親クラスに型変換してしまえば、もう親クラスからは子クラスでオーバライドしたメソッドを呼ぶ方法はありません。

オブジェクトとしての振る舞いを考えると、親から継承したオブジェクトに共通の振る舞いをさせたいとしても、その具体的な処理は子クラス毎に異なることは良くあります。例えば「図形」という親クラスがあって「面積を求める」というメソッドがあるとします。子クラスには円であったり正方形があるとすると、その具体的な計算方法は子クラスごとにマチマチです。これを「図形」クラスに対して「面積を求める」という同じメソッドで実装しようとしても親である「図形」クラスから、子クラスのそれぞれの「面積を求める」メソッドを呼び出すことができないのです。

そこで、C++には「仮想関数」という種類の関数を定義することで、親クラスから子クラスのメソッド(クラスの持つ関数)を呼び出す方法を提供しています。仮想関数を定義するのは関数定義の先頭に “virtual” というキーワードを付けるだけです。これでこのキーワードの付いた関数は親クラスへの参照やポインタであっても、そのインスタンスの実際のクラス(子クラス)の持つメソッドを呼び出すようになります。

#include <iostream>
using namespace std;

class Shape {
public:
  Shape(double length) { m_length = length; }
  virtual double Area() { return 0; } // この関数を仮想関数とする
protected:
  double m_length;
};

class Circle : public Shape {
public:
  Circle(double length) : Shape(length) {}
  double Area() { return (m_length / 2) * (m_length / 2) * 3.1415926l; }
};

class Square : public Shape {
public:
  Square(double length) : Shape(length) {}
  double Area() { return m_length * m_length; }
};

int main() {
  Circle *c = new Circle(10);
  Square *s = new Square(10);
  Shape *a[2] = { c,s };

  cout << a[0]->Area() << "," << a[1]->Area() << endl;

  delete s;
  delete c;
  return 0;
}

78.5398,100

実行結果

一旦、それぞれの子クラスのインスタンスを生成してから、そのポインタを親クラスの配列として型変換し、親クラスのメソッドとして Area() を呼び出し面積を計算しています。virtual が無ければ、親クラスのメソッドが呼ばれるので、この例では結果は

class Shape {
public:
  Shape(double length) { m_length = length; }
  double Area() { return 0; } // virtual 無し
protected:
  double m_length;
};

0,0

実行結果

となってしまいます。ちなみに virtual を付けたにも関わらず、子クラスに該当するメソッドが無ければ、その子クラスのメソッドではなく親クラスのメソッドが呼び出されます。

class Square : public Shape {
public:
  Square(double length) : Shape(length) {}
// double Area() { return m_length * m_length; }
};

78.5398,0

実行結果

このように親クラスの仮想関数が呼び出されるのを避けたい場合は、仮想関数をさらにグレードアップした「純粋仮想関数」という仕組みもあります。仮想関数は必ず子クラスのメソッドを経由して呼ぶのであれば、仮想関数自身が実行されることは無いはずなので「そんな関数は無い!」ということを少し変わった書き方で定義します。

class Shape {
public:
  Shape(double length) { m_length = length; }
  virtual double Area() = 0; // 純粋仮想関数
protected:
  double m_length;
};

この書き方をすれば親クラスの仮想関数は中身が無いので、呼び出されることはありえないので、子クラスで同じプロトタイプの関数を定義していなければ、コンパイルエラーとなります。また純粋仮想関数を持つクラス(親クラス)は、そのメソッドを呼び出すことが出来ないことから、インスタンスを作ることも出来なくなります。そのようなインスタンスを作れないクラスを「抽象クラス」と呼びます。また、親クラスのインスタンスで同じ関数を呼び出しているのに、実際に行う動作(呼び出されるメソッド)がいろいろあることを「ポリモーフィズム」とか「多態性、多様性」と呼ぶこともあります。

「C++言語」の仮想関数について理解しよう!

これで、抽象クラスの参照を引き数に取るような関数を書いても、その関数の中で実際のインスタンスの子クラスが持つメソッドを呼んでもらうことができるようになり、クラスを継承するたびに、それぞれの子クラスの参照を渡す関数を作らなくても良くなるのです。まさに「抽象化」ですね。

仮想関数

仮想関数のしくみ

さあ、ここまでは大丈夫ですか?仮想関数はとても便利で特に継承したクラスがデストラクタを持つ場合にも役立ちます。ところで親クラスの参照からどうやって実際の子クラスのメソッドを呼んでいるのか気になりませんか?それには何らかの情報を参照に持たせる必要があり、その情報を活用するためのいくつかの仕組みもあったりします。その辺りの話から次回は紐解いてみましょう。

仮想関数 (C++ のみ)

ヘッダ画像は、以下のものを使わせていただきました。https://commons.wikimedia.org/wiki/File:ISO_C%2B%2B_Logo.svg
Jeremy Kratz - https://github.com/isocpp/logos , パブリック・ドメイン,
https://commons.wikimedia.org/w/index.php?curid=62851110による

#プログラミング #プログラミング言語 #プログラミング講座 #CPP #継承 #仮想関数 #純粋仮想関数 #ポリモーフィズム #抽象クラス  

いいなと思ったら応援しよう!

kzn
頂いたチップは記事を書くための資料を揃えるために使わせていただきます!