🖥️

【C++言語入門】 第11回 抽象クラス

2025/03/05に公開

https://youtu.be/nlGEAR4VGdI

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

ずんだもん
\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{lime}{ずんだもん:} りょうかいなのだ

円も四角もまとめてしまえ

\footnotesize \textcolor{pink}{四国めたん:} さて、前回まで、 クラスの継承 と派生クラスにおける オーバーライド などについてお話ししましたわ

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

\footnotesize \textcolor{pink}{四国めたん:} その際、円を表すクラスCircleの定義などもおこなってきましたわ

\footnotesize \textcolor{lime}{ずんだもん:} そうだったのだ

\footnotesize \textcolor{pink}{四国めたん:} 一般的にプログラムを作成している場合、円を表すクラスを作成したならば、四角や三角などを表すクラスなども作成したいと思いませんか?

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

\footnotesize \textcolor{pink}{四国めたん:} そのような場合、共通するメンバ変数やメソッドなどは、まとめてしまった方が効率的になりますわ

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

\footnotesize \textcolor{pink}{四国めたん:} というわけで、円を表すクラスの 基底クラス として 図形 を表すFigureクラスを作成し、Circleクラスを継承してみますわ

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

\footnotesize \textcolor{pink}{四国めたん:} まず、次のようにしてFigureクラスを作成していきましょう

  1. ソリューションエクスプローラーを右クリック
  2. メニューから 追加クラス... を選択
  3. クラスの追加 ウィンドウで クラス名 を"Figure"に、 .hファイル を"figure.h"に変更
  4. その他のオプション:インライン をチェックしOKボタンをクリック

\footnotesize \textcolor{lime}{ずんだもん:} うん? .cpp ファイルが無いようだが?

\footnotesize \textcolor{pink}{四国めたん:} Figureクラスは然程の処理が必要ではないので、全てをヘッダーファイル内で処理する関係上、".cpp"ファイルは、いりませんわ

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

\footnotesize \textcolor{pink}{四国めたん:} はい、 その他のオプション:インライン をチェックをすると、".cpp"ファイルを作成しなくなりますわ

インラインにチェック

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

\footnotesize \textcolor{pink}{四国めたん:} "figure.h"ファイルにFigureクラスを作成しますわね

figure.h
#pragma once

#ifndef FIGURE_H
#define FIGURE_H

class Figure {
  int border_width_;
  unsigned int color_;

 public:
  Figure(int border_width, unsigned int color)
      : border_width_(border_width), color_(color) {}
  virtual ~Figure() {}

  int BorderWidth() { return border_width_; }
  void BorderWidth(int border_width) { border_width_ = border_width; }
  unsigned int Color() { return color_; }
  void Color(unsigned int color) { color_ = color; }

  virtual double Area() {
    // 処理?
  }
};

#endif  // FIGURE_H

\footnotesize \textcolor{pink}{四国めたん:} とりあえず 図形 を表す共通項目として 境界線の幅 "border_width_"と 図形の色 "color_"をメンバ変数としましたわ

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

\footnotesize \textcolor{pink}{四国めたん:} なお、それぞれに対してゲッターとセッターを作成していますわ

\footnotesize \textcolor{lime}{ずんだもん:} ゲッターとセッターは大切なのだ

\footnotesize \textcolor{pink}{四国めたん:} さて、ここで問題になるのが 図形の面積 を取得するためのAreaメソッドですわ

\footnotesize \textcolor{lime}{ずんだもん:} 処理として何も書かれていないのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、Figureを継承するクラスでは必須とも言うべきメソッドですが、Figureクラスでは宣言はできても実装することはできませんわ

\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{lime}{ずんだもん:} うむ

\footnotesize \textcolor{pink}{四国めたん:} 次に、エラーとして負数などを返す方法

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

\footnotesize \textcolor{pink}{四国めたん:} 最後にメソッドそのものを宣言、定義しない

\footnotesize \textcolor{lime}{ずんだもん:} え~?

\footnotesize \textcolor{pink}{四国めたん:} いずれの対処方法をとるにしてもプログラマが十分な注意を払う必要がありますわ

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

\footnotesize \textcolor{pink}{四国めたん:} そこでC++言語では 純粋仮想関数 という仕組みが用意されていますわ

\footnotesize \textcolor{lime}{ずんだもん:} どのようにすればいいのだ?

\footnotesize \textcolor{pink}{四国めたん:} このような形式でメソッドの宣言を行うのですわ

virtual 型 メソッド名(引数...) = 0;

\footnotesize \textcolor{lime}{ずんだもん:} virtualを伴った 仮想関数 の宣言で、最後に= 0を付加するのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、これにより 派生クラス でオーバーライドが必須となりますわ

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

\footnotesize \textcolor{pink}{四国めたん:} それでは 純粋仮想関数 を用いてFigureクラスを書き換えてみましょう

figure.h
#pragma once

#ifndef FIGURE_H
#define FIGURE_H

class Figure {
  int border_width_;
  unsigned int color_;

 public:
  Figure(int border_width, unsigned int color)
      : border_width_(border_width), color_(color) {}
  virtual ~Figure() {}

  int BorderWidth() { return border_width_; }
  void BorderWidth(int border_width) { border_width_ = border_width; }
  unsigned int Color() { return color_; }
  void Color(unsigned int color) { color_ = color; }

  virtual double Area() = 0;
};

#endif  // FIGURE_H

\footnotesize \textcolor{pink}{四国めたん:} なお 純粋仮想関数 が宣言されているクラスを 抽象クラス と呼びますわ

\footnotesize \textcolor{lime}{ずんだもん:} 抽象クラス

\footnotesize \textcolor{pink}{四国めたん:} はい、 抽象クラス はインスタンスを直接生成することはできませんわ

\footnotesize \textcolor{lime}{ずんだもん:} 間接的には生成できるのか?

\footnotesize \textcolor{pink}{四国めたん:} 派生クラス のインスタンスへの 参照ポインタ で宣言可能ですわ

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

\footnotesize \textcolor{pink}{四国めたん:} まぁ、当然といえば当然ですわね

\footnotesize \textcolor{pink}{四国めたん:} 抽象クラス のインスタンスを生成しても、 純粋仮想関数 の実体がないのでメソッドをコールできませんから...

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

\footnotesize \textcolor{lime}{ずんだもん:} ところで 参照 とはなんなのだ?

\footnotesize \textcolor{pink}{四国めたん:} 参照 については次回にお話ししますわ

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

\footnotesize \textcolor{pink}{四国めたん:} それではFigureクラスを継承したCircleクラスを見てみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>
#include "figure.h"

const double kPI = 3.14159265358979323846;
const int kMessageSize = 50;

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

 public:
  Circle(double diameter) : Figure(1, 0xFF000000), 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

\footnotesize \textcolor{pink}{四国めたん:} まず、コンストラクタでは、初期化子リストで 基底クラス のコンストラクタを呼び出して初期化していますわ

\footnotesize \textcolor{lime}{ずんだもん:} Figureの初期化にリテラルを渡しているようだが...

\footnotesize \textcolor{pink}{四国めたん:} 本来 境界線の幅図形の色 などもCircleのコンストラクタで渡すのが普通ですわ

\footnotesize \textcolor{pink}{四国めたん:} でも、今回は横着して数値を直接渡していますわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむ、では初期化値の"0xFF000000"について教えて欲しいのだ

\footnotesize \textcolor{pink}{四国めたん:} "color"として渡した"0xFF000000"は色をunsigned int型で渡すときによく使われる記法で、色を8桁の16進数として表記したものですわ

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

\footnotesize \textcolor{pink}{四国めたん:} はい、通常"0xFF000000"は"黒"を表しますわ

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

\footnotesize \textcolor{pink}{四国めたん:} ではメイン関数を書き直して実行してみましょう

hello_world.cpp
#include <iostream>
#include "circle.h"

int main(int argc, char* argv[]) {
  Circle c(10.0);
  std::cout << "円の直径は" << c.Diameter() << "cmです。" << std::endl;
  std::cout << "円の境界線の幅は" << c.BorderWidth() << "mmです。" << std::endl;
  std::cout << "円の面積は" << c.Area() << "cm^2です。" << std::endl;
  return 0;
}

抽象クラス

\footnotesize \textcolor{lime}{ずんだもん:} とりあえず問題なく表示できたのだ

\footnotesize \textcolor{pink}{四国めたん:} ここでCircleクラスのAreaメソッドをコメントアウトしてみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>
#include <stdexcept>
#include "figure.h"

const double kPI = 3.14159265358979323846;
const int kMessageSize = 50;

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

 public:
  Circle(double diameter) : Figure(1, 0xFF000000), 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

エラー

\footnotesize \textcolor{lime}{ずんだもん:} 抽象クラス型"Circle"のオブジェクトは使用できません というエラーが発生したのだ

\footnotesize \textcolor{pink}{四国めたん:} 純粋仮想関数 であるAreaをオーバーライドしていないため、Circleクラスも 抽象クラス とみなされているのですわ

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

純粋仮想関数を定義する

ところで 純粋仮想関数 に定義を記述することはできるのでしょうか?

例えばFigureクラスのAreaメソッドについて、以下のようにすることはOKなのでしょうか?

virtual double Area() = 0 {
  return 0.0;
}

Visual Studioでは問題ないようです

ただ、これは独自の拡張仕様のようで、一般的なC++言語ではエラーになったり推奨されなかったりするようです

あまり意味がない仕様なので、使わない方が良いと思います

デストラクタを純粋仮想関数に?

\footnotesize \textcolor{lime}{ずんだもん:} ちなみに デストラクタ純粋仮想関数 にすることはできるのか?

\footnotesize \textcolor{pink}{四国めたん:} 例えばFigureクラスでこのようにしたいということですか?

virtual ~Figure() = 0;

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

\footnotesize \textcolor{pink}{四国めたん:} 結論から言うと 絶対にダメ ですわ

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

\footnotesize \textcolor{pink}{四国めたん:} 基底クラスデストラクタ派生クラス のインスタンスが破棄される際にも必ず呼ばれますわ

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

\footnotesize \textcolor{pink}{四国めたん:} そのため、実体としての定義がないと不具合が生じますわ

デストラクタのエラー

まとめ

\footnotesize \textcolor{pink}{四国めたん:} 以上で 抽象クラス について終わりますわ

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

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

Discussion