🖥️

【C++言語入門】 第10回 オーバーライドとポリモーフィズム

に公開

https://youtu.be/aoCrVWB28po

四国めたん
\textcolor{pink}{四国めたん: }教師役ですわ

ずんだもん
\textcolor{lime}{ずんだもん: }生徒役なのだ

\footnotesize \textcolor{pink}{四国めたん:} こんにちは。四国めたんです

\footnotesize \textcolor{lime}{ずんだもん:} ずんだもんなのだ。こんにちはなのだ

\footnotesize \textcolor{pink}{四国めたん:} 今回はクラスのメソッドの オーバーライドポリモーフィズム についてお話ししますわ

\footnotesize \textcolor{lime}{ずんだもん:} オーバーライドポリモーフィズム

\footnotesize \textcolor{pink}{四国めたん:} クラスのメソッドに対する オーバーライド はC++言語で ポリモーフィズム を実現するための重要な機能の1つですわ

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、さっそくその辺りをお話ししますわね。

\footnotesize \textcolor{lime}{ずんだもん:} お願いするのだ

基底クラスのメソッドを上書きしましょう

\footnotesize \textcolor{pink}{四国めたん:} 前回は クラス継承 について、簡単にお話ししましたわ

\footnotesize \textcolor{lime}{ずんだもん:} 覚えているのだ

\footnotesize \textcolor{pink}{四国めたん:} その中で 基底クラス 中のメソッドと同じ名前、同じ引数のメソッドを 派生クラス で定義していましたわ

\footnotesize \textcolor{lime}{ずんだもん:} その辺りの説明を今回まで引き延ばされてしまっていたのだ

\footnotesize \textcolor{pink}{四国めたん:} お待たせしましたが、さっそく説明していきますわね

\footnotesize \textcolor{pink}{四国めたん:} まず、前回のプログラム例を見てみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;

/// @brief 円
class Circle {
  double diameter_;  // 直径

 public:
  Circle(double diameter) : diameter_(diameter) {}
  ~Circle() {}

  double Diameter() { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  double Area() {
    double radius = Diameter() / 2.0;
    double a = radius * radius * kPI;
    return a;
  }
};

#endif  // CIRCLE_H
ellipse.h
#pragma once

#ifndef ELLIPSE_H
#define ELLIPSE_H

#include "circle.h"

/// @brief 楕円
class Ellipse : public Circle {
  double long_diameter_;  // 長径

 public:
  Ellipse(double short_diameter, double long_diameter)
      : Circle(short_diameter), long_diameter_(long_diameter) {}
  ~Ellipse() {}

  double LongDiameter() { return long_diameter_; }
  void LongDiameter(double long_diameter) { long_diameter_ = long_diameter; }

  double Area() {
    double short_radius = Diameter() / 2.0;
    double long_radius = LongDiameter() / 2.0;
    double a = short_radius * long_radius * kPI;
    return a;
  }
};

#endif  // ELLIPSE_H
hello_world.cpp
#include <iostream>

#include "ellipse.h"

int main(int argc, char* argv[]) {
  Ellipse* pe = new Ellipse(10.0, 15.0);
  std::cout << "楕円の長径は" << pe->LongDiameter() << "cmです。" << std::endl;
  std::cout << "楕円の短径は" << pe->Diameter() << "cmです。" << std::endl;
  std::cout << "楕円の面積は" << pe->Area() << "cm^2です" << std::endl;
  delete pe;
  pe = nullptr;
  return 0;
}

\footnotesize \textcolor{lime}{ずんだもん:} double Area()というメソッドがCircleとその派生クラスEllipseで定義されているのだ

\footnotesize \textcolor{pink}{四国めたん:} はい オーバーライド の回で説明しましたが、C++言語では同じ名前のメソッドであっても引数が異なれば許されていますわ

\footnotesize \textcolor{lime}{ずんだもん:} おぼえているのだ

\footnotesize \textcolor{lime}{ずんだもん:} ただ、今回の場合は名前も引数も同じメソッドなのだ

\footnotesize \textcolor{pink}{四国めたん:} その通りですわ

\footnotesize \textcolor{pink}{四国めたん:} ただC++言語では 基底クラス で定義されたメソッドを 派生クラス 内で同じ名前、同じ引数で定義して上書きすることができるのですわ

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、これをメソッドの オーバーライド と云いますわ

\footnotesize \textcolor{lime}{ずんだもん:} オーバーロード と紛らわしいのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、似た響きなので間違わないようにしてくださいね

\footnotesize \textcolor{lime}{ずんだもん:} う~ん...

\footnotesize \textcolor{pink}{四国めたん:} 「上書き = 上から乗せる(ライド)」と覚えれば良いかもしれませんわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

C++言語の重要な概念ポリモーフィズム

\footnotesize \textcolor{pink}{四国めたん:} ところでC++言語などのオブジェクト指向なプログラミング言語では、 ポリモーフィズム (多態性)という概念が重要になってきますわ

\footnotesize \textcolor{lime}{ずんだもん:} ポリモーフィズム

\footnotesize \textcolor{pink}{四国めたん:} まぁ、簡単に言うと、同じ名前や同じ属性であっても、使われる場所や条件によって動作を変えることですわ

\footnotesize \textcolor{lime}{ずんだもん:} よくわからないのだ

\footnotesize \textcolor{pink}{四国めたん:} 例えば オーバーロードオーバーライド では、同じ名前の関数やメソッドでも引数やクラスの違いで動作を変えることができますわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむふむ

\footnotesize \textcolor{pink}{四国めたん:} プログラム例で云えば、同じAreaと云うメソッドでもCircleクラスのインスタンスで呼ばれたか、Ellipseクラスのインスタンスで呼ばれたかによって面積の計算方法が異なりますわね

\footnotesize \textcolor{lime}{ずんだもん:} たしかにその通りなのだ

\footnotesize \textcolor{pink}{四国めたん:} 少しプログラム例のメイン関数を変えて見てみましょう

hello_world.cpp
#include <iostream>

#include "ellipse.h"

int main(int argc, char* argv[]) {
  Ellipse* pe = new Ellipse(10.0, 15.0);
  Circle* pc = pe;
  std::cout << "楕円の長径は" << pe->LongDiameter() << "cmです。" << std::endl;
  std::cout << "楕円の短径は" << pe->Diameter() << "cmです。" << std::endl;
  std::cout << "楕円の面積は" << pe->Area() << "cm^2です" << std::endl;
  std::cout << "円の面積は" << pc->Area() << "cm^2です" << std::endl;
  delete pe;
  pc = nullptr;
  pe = nullptr;
  return 0;
}

ポリモーフィズム

\footnotesize \textcolor{lime}{ずんだもん:} おや?メイン関数内でEllipseクラスとして生成したインスタンスをCircleクラスのポインタに代入しているのだ

\footnotesize \textcolor{pink}{四国めたん:} 少し不思議な感じですわね

\footnotesize \textcolor{lime}{ずんだもん:} 問題はないのか?

\footnotesize \textcolor{pink}{四国めたん:} 問題ありませんわ

\footnotesize \textcolor{pink}{四国めたん:} EllipseクラスはCircleクラスを 継承 していますので、Circleクラスの全てを内包していますわ

\footnotesize \textcolor{lime}{ずんだもん:} たしかに...

\footnotesize \textcolor{pink}{四国めたん:} ですのでEllipseクラスのインスタンスはCircleクラスのインスタンスとして扱っても問題ないのですわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} 結果としてCircleのポインタ"pc"からAreaメソッドを呼び出すとCircleクラス内で定義されたAreaメソッドが呼び出されますわ

元のクラスのメソッドを呼び出したいのですが...

\footnotesize \textcolor{lime}{ずんだもん:} でも、もともとEllipseのインスタンスとして生成したのだから、Circleのポインタから呼び出しても上書きしたEllipseクラスのAreaメソッドが呼び出されてほしいのだ

\footnotesize \textcolor{pink}{四国めたん:} まぁ、そういう場合もありますわね

\footnotesize \textcolor{pink}{四国めたん:} そのために用意されたのがvirtualキーワードですわ

\footnotesize \textcolor{lime}{ずんだもん:} virtual

\footnotesize \textcolor{pink}{四国めたん:} 基本的にはメソッドの宣言もしくは定義の戻り値の型の前に付加しますわ

virtual 型 メソッド名(引数...) {処理}

\footnotesize \textcolor{pink}{四国めたん:} このようにvirtualキーワードを付加されたメソッドを 仮想関数 もしくは 仮想メソッド と云いますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} なおvirtualキーワードは 基底クラス でのみ付加すればOKですわ

\footnotesize \textcolor{lime}{ずんだもん:} 派生クラス で付けてはいけないのか?

\footnotesize \textcolor{pink}{四国めたん:} 派生クラス で付けても間違いではありませんわ

\footnotesize \textcolor{pink}{四国めたん:} むしろ 仮想メソッド であることを明示するためにもできるだけ付加したほうがいいですわね

\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

\footnotesize \textcolor{pink}{四国めたん:} それではプログラム例でvirtualキーワードを使ってみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;

/// @brief 円
class Circle {
  double diameter_;  // 直径

 public:
  Circle(double diameter) : diameter_(diameter) {}
  ~Circle() {}

  double Diameter() { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual double Area() {
    double radius = Diameter() / 2.0;
    double a = radius * radius * kPI;
    return a;
  }
};

#endif  // CIRCLE_H
ellipse.h
#pragma once

#ifndef ELLIPSE_H
#define ELLIPSE_H

#include "circle.h"

/// @brief 楕円
class Ellipse : public Circle {
  double long_diameter_;  // 長径

 public:
  Ellipse(double short_diameter, double long_diameter)
      : Circle(short_diameter), long_diameter_(long_diameter) {}
  ~Ellipse() {}

  double LongDiameter() { return long_diameter_; }
  void LongDiameter(double long_diameter) { long_diameter_ = long_diameter; }

  virtual double Area() {
    double short_radius = Diameter() / 2.0;
    double long_radius = LongDiameter() / 2.0;
    double a = short_radius * long_radius * kPI;
    return a;
  }
};

#endif  // ELLIPSE_H

仮想メソッド

\footnotesize \textcolor{pink}{四国めたん:} しっかりと楕円の面積が出力されていますわね

\footnotesize \textcolor{lime}{ずんだもん:} 今回はCircleのインスタンスから呼ばれたAreaメソッドはEllipseのメソッドとなったのだ

デストラクタも仮想メソッドに

\footnotesize \textcolor{pink}{四国めたん:} ところでプログラム例では生成されたEllipseのインスタンスを破棄するためにEllipseへのポインタに対してdeleteしていますわ

\footnotesize \textcolor{lime}{ずんだもん:} 当然なのだ

\footnotesize \textcolor{pink}{四国めたん:} では、同じインスタンスを指しているCircleへのポインタ"pc"に対してdeleteすることはできるのでしょうか?

\footnotesize \textcolor{lime}{ずんだもん:} わからないのだ

\footnotesize \textcolor{pink}{四国めたん:} とりあえず確認してみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;

/// @brief 円
class Circle {
  double diameter_;  // 直径

 public:
  Circle(double diameter) : diameter_(diameter) {}
  ~Circle() { std::cout << "~Circleが呼ばれました" << std::endl; }

  double Diameter() { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual double Area() {
    double radius = Diameter() / 2.0;
    double a = radius * radius * kPI;
    return a;
  }
};

#endif  // CIRCLE_H
ellipse.h
#pragma once

#ifndef ELLIPSE_H
#define ELLIPSE_H

#include "circle.h"

/// @brief 楕円
class Ellipse : public Circle {
  double long_diameter_;  // 長径

 public:
  Ellipse(double short_diameter, double long_diameter)
      : Circle(short_diameter), long_diameter_(long_diameter) {}
  ~Ellipse() { std::cout << "~Ellipseが呼ばれました" << std::endl; }

  double LongDiameter() { return long_diameter_; }
  void LongDiameter(double long_diameter) { long_diameter_ = long_diameter; }

  virtual double Area() {
    double short_radius = Diameter() / 2.0;
    double long_radius = LongDiameter() / 2.0;
    double a = short_radius * long_radius * kPI;
    return a;
  }
};

#endif  // ELLIPSE_H
hello_world.cpp
#include <iostream>

#include "ellipse.h"

int main(int argc, char* argv[]) {
  Ellipse* pe = new Ellipse(10.0, 15.0);
  Circle* pc = pe;
  std::cout << "楕円の長径は" << pe->LongDiameter() << "cmです。" << std::endl;
  std::cout << "楕円の短径は" << pe->Diameter() << "cmです。" << std::endl;
  std::cout << "楕円の面積は" << pe->Area() << "cm^2です" << std::endl;
  std::cout << "円の面積は" << pc->Area() << "cm^2です" << std::endl;
  delete pc;
  pc = nullptr;
  pe = nullptr;
  return 0;
}

デストラクタ

\footnotesize \textcolor{lime}{ずんだもん:} コンソール出力を見るとCircleのデストラクタは呼ばれてもEllipseのデストラクタは呼ばれていないのだ

\footnotesize \textcolor{pink}{四国めたん:} Ellipseのインスタンスなので、Ellipseのデストラクタが呼ばれないとメモリリークになってしまいますわ

\footnotesize \textcolor{lime}{ずんだもん:} むぅ~、問題なのだ

\footnotesize \textcolor{pink}{四国めたん:} これはdeleteをコールした際にデストラクタが 仮想メソッド として指定されていないためにCircleのデストラクタが直接呼ばれることで発生していますわ

\footnotesize \textcolor{lime}{ずんだもん:} 何とかならないのか?

\footnotesize \textcolor{pink}{四国めたん:} 正常に動作させるためには、デストラクタにvirtualキーワードを付加して仮想化する必要がありますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;

/// @brief 円
class Circle {
  double diameter_;  // 直径

 public:
  Circle(double diameter) : diameter_(diameter) {}
  virtual ~Circle() { std::cout << "~Circleが呼ばれました" << std::endl; }

  double Diameter() { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual double Area() {
    double radius = Diameter() / 2.0;
    double a = radius * radius * kPI;
    return a;
  }
};

#endif  // CIRCLE_H
ellipse.h
#pragma once

#ifndef ELLIPSE_H
#define ELLIPSE_H

#include "circle.h"

/// @brief 楕円
class Ellipse : public Circle {
  double long_diameter_;  // 長径

 public:
  Ellipse(double short_diameter, double long_diameter)
      : Circle(short_diameter), long_diameter_(long_diameter) {}
  virtual ~Ellipse() { std::cout << "~Ellipseが呼ばれました" << std::endl; }

  double LongDiameter() { return long_diameter_; }
  void LongDiameter(double long_diameter) { long_diameter_ = long_diameter; }

  virtual double Area() {
    double short_radius = Diameter() / 2.0;
    double long_radius = LongDiameter() / 2.0;
    double a = short_radius * long_radius * kPI;
    return a;
  }
};

#endif  // ELLIPSE_H

仮想化デストラクタ

\footnotesize \textcolor{lime}{ずんだもん:} 今度はCircleEllipseのデストラクタが順番に呼び出されたのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、このように全てのデストラクタが呼び出されるように、特別な理由がない限り、デストラクタには必ずvirtualを付けてくださいね

\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

まとめ

\footnotesize \textcolor{pink}{四国めたん:} 以上で オーバーライドとポリモーフィズム を終了しますわ

\footnotesize \textcolor{lime}{ずんだもん:} おつかれさまなのだ

\footnotesize \textcolor{pink}{四国めたん:} お疲れさまでした

Discussion