🖥️

【C++言語入門】 第7回 ファイル毎にクラスを

2025/02/25に公開

https://youtu.be/nTyFcwU0oeU

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} 今回はファイル毎にクラスを作成することについてお話ししますわ

\footnotesize \textcolor{lime}{ずんだもん:} C言語では関数を別のファイルにまとめることができたのだ

\footnotesize \textcolor{pink}{四国めたん:} そうですわね

\footnotesize \textcolor{pink}{四国めたん:} メンバ変数やメソッドがまとまっている クラス をファイルにまとめるのは理にかなっていると思いませんか?

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

クラスを別ファイルに

\footnotesize \textcolor{pink}{四国めたん:} 前回までは、クラスはメイン関数と同じ"hello_world.cpp"ファイルに定義していましたわ

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

\footnotesize \textcolor{pink}{四国めたん:} ただ、プログラムのサイズが大きくなって複数のクラスを定義するようになると、ファイルサイズが大きくなっていろいろと不具合が出てきますわ

\footnotesize \textcolor{lime}{ずんだもん:} そのあたりの事情はC言語と変わりがないのだ

\footnotesize \textcolor{pink}{四国めたん:} ですので種類としてまとめておいた方が良い定数や列挙子、関数、構造体などは、別ファイルにしておくほうが良いですわ

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

\footnotesize \textcolor{pink}{四国めたん:} 特に クラス については関係するデータや関数は メンバ変数メソッド として内包していますので、クラス毎にファイルとしてまとめておくと管理が容易になりますわ

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえず前々回の 動的メモリ確保 で使ったプログラム例のCircleクラスを別ファイルに移してみましょう

  1. まず、Circleクラス全体をコメントアウト
  2. ソリューションエクスプローラーで Project2 を右クリック
  3. メニューから 追加...クラス... を選択
  4. クラスの追加 ダイアログボックスの クラス名 に"Circle"、 .hファイル を"circle.h"に、 .cppファイル を"circle.cpp"に変更
  5. 基底クラス は空欄、 インライン がチェックされていないことを確認してOKをクリック
  6. 新規に作成された"circle.h"にインクルードガード(CIRCLE_H)を追加
  7. "circle.h"にCircleクラス全体を移動しコメントアウトを解除
  8. "circle.h"に定数を移動しヘッダー"iostream"を追加
  9. "hello_world.cpp"に"circle.h"ヘッダーファイルをインクルード

\footnotesize \textcolor{pink}{四国めたん:} まず、"hello_world.cpp"内のCircleクラスは必要ないので、コメントアウトしておきますわ

#include <iostream>

//const double kPI = 3.14159265358979323846;
//const int kMessageSize = 50;
//
///// @brief 円
//class Circle {
//  double diameter_;      // 直径
//  double border_width_;  // 境界線の幅
//  char* pmessage_;
//
// public:
//  Circle(double diameter, double border_width)
//      : diameter_(diameter), border_width_(border_width), pmessage_(nullptr) {
//    pmessage_ = new char[kMessageSize];
//  }
//  ~Circle() {
//    std::cout << "デストラクタが呼ばれました" << std::endl;
//    delete[] pmessage_;
//    pmessage_ = nullptr;
//  }
//
//  double Diameter() { return diameter_; }
//  void Diameter(double diameter) { diameter_ = diameter; }
//  double BorderWidth() { return border_width_; }
//  void BorderWidth(double border_width) { border_width_ = border_width; }
//
//  double Area() {
//    double radius = Diameter() / 2.0;
//    double a = radius * radius * kPI;
//    return a;
//  }
//
//  const char* Message() {
//    double area = Area();
//    sprintf_s(pmessage_, kMessageSize, "円の面積は%fです。", area);
//    return pmessage_;
//  }
//};

int main(int argc, char* argv[]) {
  Circle* pc = new Circle(10.0, 1.0);
  std::cout << "円の直径は" << pc->Diameter() << "cmです。" << std::endl;
  std::cout << pc->Message() << std::endl;
  delete pc;
  pc = nullptr;
  return 0;
}

\footnotesize \textcolor{lime}{ずんだもん:} なぜなのだ?

\footnotesize \textcolor{pink}{四国めたん:} Circleクラスが残っていると新たにCircleクラスを作成できないのですわ

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

\footnotesize \textcolor{pink}{四国めたん:} 次にソリューションエクスプローラーで"Project2"を右クリックしますわ

\footnotesize \textcolor{lime}{ずんだもん:} メニューが出て来たのだ

\footnotesize \textcolor{pink}{四国めたん:} メニューから 追加...クラス... を選択しますわ

メニュー選択

\footnotesize \textcolor{lime}{ずんだもん:} クラスの追加 ダイアログボックスが表示されたのだ

\footnotesize \textcolor{pink}{四国めたん:} クラス名 に"Circle"、 .hファイル を"circle.h"に、 .cppファイル を"circle.cpp"にしますわ

\footnotesize \textcolor{lime}{ずんだもん:} その他は変更なしで良いのか

クラスの追加ダイアログ

\footnotesize \textcolor{pink}{四国めたん:} はい、OKをクリックすると"circle.h"と"circle.cpp"ファイルが作成されますわ

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

\footnotesize \textcolor{pink}{四国めたん:} "circle.h"ファイルを変更していきますわ

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

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

/// @brief 円
class Circle {
  double diameter_;      // 直径
  double border_width_;  // 境界線の幅
  char* pmessage_;

 public:
  Circle(double diameter, double border_width)
      : diameter_(diameter), border_width_(border_width), pmessage_(nullptr) {
    pmessage_ = new char[kMessageSize];
  }
  ~Circle() {
    std::cout << "デストラクタが呼ばれました" << std::endl;
    delete[] pmessage_;
    pmessage_ = nullptr;
  }

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

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

  const char* Message() {
    double area = Area();
    sprintf_s(pmessage_, kMessageSize, "円の面積は%fです。", area);
    return pmessage_;
  }
};

#endif  // CIRCLE_H

\footnotesize \textcolor{pink}{四国めたん:} 次に"hello_world.cpp"ですわ

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

int main(int argc, char* argv[]) {
  Circle* pc = new Circle(10.0, 1.0);
  std::cout << "円の直径は" << pc->Diameter() << "cmです。" << std::endl;
  std::cout << pc->Message() << std::endl;
  delete pc;
  pc = nullptr;
  return 0;
}

\footnotesize \textcolor{lime}{ずんだもん:} "circle.cpp"は変更しなくてもOKなのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、"circle.cpp"は現状変更なしですわ

\footnotesize \textcolor{pink}{四国めたん:} では実行してみましょう

別ファイルのクラス

\footnotesize \textcolor{lime}{ずんだもん:} 特に以前と変わらないのだ

メソッドを別ファイルに

\footnotesize \textcolor{lime}{ずんだもん:} "circle.cpp"ファイルが空白のままなのが気になるのだが...

\footnotesize \textcolor{lime}{ずんだもん:} このファイルはどのように使うのだ?

\footnotesize \textcolor{pink}{四国めたん:} 実はコンストラクタやデストラクタを含む メソッド はヘッダーファイルのクラスの定義内では宣言のみを行うのが一般的なのですわ

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

\footnotesize \textcolor{pink}{四国めたん:} はい、メソッドの実際の定義は別の".cpp"ファイル内で行うのですわ

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

\footnotesize \textcolor{pink}{四国めたん:} ですので一般的には"circle.cpp"でコンストラクタやデストラクタ、AreaメソッドやMessageメソッドの定義を行うのですわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほど、"circle.h"ではCircleクラスのコンストラクタやデストラクタ、AreaメソッドやMessageメソッドの宣言をおこなうのか

\footnotesize \textcolor{pink}{四国めたん:} はい、とりあえず書き直してみますわ

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

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

/// @brief 円
class Circle {
  double diameter_;      // 直径
  double border_width_;  // 境界線の幅
  char* pmessage_;

 public:
  Circle(double diameter, double border_width);
  ~Circle();

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

  double Area();
  const char* Message();
};

#endif  // CIRCLE_H
circle.cpp
#include "circle.h"

Circle::Circle(double diameter, double border_width)
    : diameter_(diameter), border_width_(border_width), pmessage_(nullptr) {
  pmessage_ = new char[kMessageSize];
}

Circle::~Circle() {
  std::cout << "デストラクタが呼ばれました" << std::endl;
  delete[] pmessage_;
  pmessage_ = nullptr;
}

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

const char* Circle::Message() {
  double area = Area();
  sprintf_s(pmessage_, kMessageSize, "円の面積は%fです。", area);
  return pmessage_;
}

\footnotesize \textcolor{pink}{四国めたん:} "hello_world.cpp"の内容は変わりませんわ

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえず実行してみますわ

別ファイルでの定義

\footnotesize \textcolor{lime}{ずんだもん:} 問題なく実行できたのだ

\footnotesize \textcolor{pink}{四国めたん:} なお、ゲッターとセッターについては、クラスの定義内で定義を行うほうがいいですわ

メソッドの定義の方法は?

\footnotesize \textcolor{lime}{ずんだもん:} ところで"circle.cpp"ファイルでのメソッドの定義方法はおなじなのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、メソッドの定義の方法は元のヘッダーファイルでの定義と変わりませんわ

\footnotesize \textcolor{pink}{四国めたん:} ただ、メソッド名の前に スコープ解決演算子 "::"を挟んでクラス名を指定しますわ

\footnotesize \textcolor{lime}{ずんだもん:} ネームスペース を指定する時と同じなのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、 スコープ解決演算子クラス の指定にも使えますわね

\footnotesize \textcolor{lime}{ずんだもん:} 具体的にはどのような指定になるのだ?

\footnotesize \textcolor{pink}{四国めたん:} 例えば型 クラス名::メソッド名(引数...) {処理 }のようになりますわ

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

クラス内のメソッドの宣言の方法は?

\footnotesize \textcolor{lime}{ずんだもん:} ちなみに"circle.h"内のメソッドの宣言はどのようになるのだ?

\footnotesize \textcolor{pink}{四国めたん:} メソッドの宣言については、基本的には波括弧"{}"で括られた処理部分を削除してセミコロン";"を付加しますわ

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

\footnotesize \textcolor{pink}{四国めたん:} こちらは クラス の定義内にあるというだけで、通常の関数の宣言と変わりませんわね

メソッドの定義を別ファイルにするメリットは?

メソッドの定義をクラスのヘッダーファイルではなく、ソースファイルで行うのには何かメリットがあるのでしょうか

有名どころのプログラム言語、例えば JavaPythonC# などは、基本的に定義と宣言を分けません

ヘッダーファイルに宣言、ソースファイルに定義と分離するのはC言語やC++言語の特徴のひとつでしょう

ヘッダーファイルとソースファイルに宣言と定義を分ける主なメリットは以下の通りです

  1. プログラム生成時間の短縮

メソッドの定義を修正した場合、ヘッダーファイルが宣言のみで変更が無ければ、ヘッダーファイルをインクルードしたソースファイルの再コンパイルは必要ありません。

もしヘッダーファイルに定義も含んでいた場合には、全てのソースファイルを再コンパイルする必要があるため、大規模なプログラムでは生成時間が問題となってきます

  1. モジュールの管理

ライブラリを作成、提供する際、ヘッダーファイルにメソッドの定義が書かれていると、実装内容が公開されることになるため、メソッドを修正し難くなります

ヘッダーファイルに宣言のみを記述していれば、メソッドの定義の修正はライブラリの利用者に及ぼす影響を最小限に抑えられます

なお、ヘッダーファイルの宣言とソースファイルの定義の整合性や宣言と定義の両方に必要な記述の分の作業量が増えるなどのデメリットも存在します

最近の高速なPCで大規模ではないプログラムを作る限りでは、あまりメリットを感じないかもしれません

インライン関数は特殊ですよ

\footnotesize \textcolor{pink}{四国めたん:} 次は インライン関数 についてお話ししますわ

\footnotesize \textcolor{lime}{ずんだもん:} インライン関数

\footnotesize \textcolor{pink}{四国めたん:} はい、関数の定義の前にinlineキーワードを付加した関数を インライン関数 と呼びますわ

\footnotesize \textcolor{lime}{ずんだもん:} どのような機能があるのだ?

\footnotesize \textcolor{pink}{四国めたん:} その関数の処理内容が、関数コールの場所に展開されますわ

\footnotesize \textcolor{lime}{ずんだもん:} 展開 と云うことは置き換えられると云うことか?

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

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

\footnotesize \textcolor{pink}{四国めたん:} 例えば次のようなコードがあったとします

#include <iostream>

void print() {
  std::cout << "これはテストです。" << std::endl;
  return;
}

int main(int argc, char* argv[]) {
  print();
  return 0;
}

\footnotesize \textcolor{lime}{ずんだもん:} コンソールに「これはテストです。」と表示するだけのプログラムなのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、メイン関数でprint関数がコールされた際には、print関数が呼ばれるプログラムですわね

\footnotesize \textcolor{pink}{四国めたん:} ただ、次のようにinlineキーワードが付加されると状況が変わりますわ

#include <iostream>

inline void print() {
  std::cout << "これはテストです。" << std::endl;
  return;
}

int main(int argc, char* argv[]) {
  print();
  return 0;
}

\footnotesize \textcolor{lime}{ずんだもん:} どのように変わるのだ?

\footnotesize \textcolor{pink}{四国めたん:} このコードはこのように解釈されますわ

int main(int argc, char* argv[]) {
  std::cout << "これはテストです。" << std::endl;
  return 0;
}

\footnotesize \textcolor{lime}{ずんだもん:} print();の部分がprint関数の処理内容で置き換わって、print関数がなくなっているのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、このように置き換えられると、関数を呼び出す際のオーバーヘッドがなくなり、高速化が期待できますわ

\footnotesize \textcolor{lime}{ずんだもん:} お~、すごいのだ!

\footnotesize \textcolor{pink}{四国めたん:} 一方で、多くの場所で インライン関数 を呼び出すと、関数による処理の共通化が無い分、コードサイズが大きくなりがちですわね

\footnotesize \textcolor{lime}{ずんだもん:} う~ん、デメリットもしっかりとあるのだ

\footnotesize \textcolor{pink}{四国めたん:} ちなみにインライン関数の処理内容が複雑だったり大きかったりする場合には、コンパイラの方で通常の関数として扱うように変更したりしますわ

\footnotesize \textcolor{lime}{ずんだもん:} そのあたりは適当なのだな

\footnotesize \textcolor{pink}{四国めたん:} なお、クラスの定義内で定義されたメソッドは、自動的にinlineキーワードを付加された状態とみなされますわ

\footnotesize \textcolor{pink}{四国めたん:} つまりゲッターやセッターは、基本的にはただの代入なので、インライン関数には向いていると云えますわ

\footnotesize \textcolor{lime}{ずんだもん:} だからゲッターやセッターをクラスの定義の中に残したのだな

まとめ

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

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

\footnotesize \textcolor{pink}{四国めたん:} 以上で ファイル毎にクラスを を終了しますわ

Discussion