基本的に怠Diary

主に日常と作ったものを書いていく。

一週間でC++の基礎が学べる本 三日目

オブジェクト指向

今日はC++クラスについて学んだ。学生の成績管理を題材にして、オブジェクト指向プログラミングの基礎を体験してみた。

なお、コードについては技術書そのまま使うのは憚られるため、生成AIに別のコードを用意してもらった。

コード全体

main.cpp

#include <iostream>
#include "student.h"
using namespace std;

int main(int argc, char** argv) {
  Student student1, student2, student3, student4;
  student1.name = "田山太郎";      // 名前を設定
  student1.score = 50;            // 点数を設定
  student1.showReport(1);         // 第1回テストの結果として表示

  student2.name = "中田太郎";      // 名前を設定
  student2.score = 95;            // 点数を設定
  student2.showReport(2);         // 第2回テストの結果として表示

  student3.name = "田中太郎";      // 名前を設定
  student3.score = 85;            // 点数を設定
  student3.showReport(3);         // 第3回テストの結果として表示

  student4.name = "田中太郎";      // 名前を設定
  student4.score = 61;            // 点数を設定
  student4.showReport(99);         // 第99回テストの結果として表示
  return 0;
}

student.h(ヘッダーファイル)

#ifndef STUDENT_H_
#define STUDENT_H_

#include <string>

class Student {
public:
  std::string name;                    // メンバ変数(学生の名前)
  int score;                          // メンバ変数(テストの点数)
  void showReport(int testNumber);    // メンバ関数(成績表示)
};

#endif // STUDENT_H_

student.cpp(実装ファイル)

#include "student.h"
#include <iostream>
using namespace std;

void Student::showReport(int testNumber){
  cout << "=== 第" << testNumber << "回テスト結果 ===" << endl;
  cout << "生徒名: " << name << endl;
  cout << "得点: " << score << "点" << endl;
  
  // 成績判定
  if (score >= 90) {
    cout << "評価: A (優秀)" << endl;
  } else if (score >= 70) {
    cout << "評価: B (良好)" << endl;
  } else if (score >= 60) {
    cout << "評価: C (可)" << endl;
  } else {
    cout << "評価: D (要努力)" << endl;
  }
}

実行結果

=== 第1回テスト結果 ===
生徒名: 田山太郎
得点: 50点
評価: D (要努力)
=== 第2回テスト結果 ===
生徒名: 中田太郎
得点: 95点
評価: A (優秀)
=== 第3回テスト結果 ===
生徒名: 田中太郎
得点: 85点
評価: B (良好)
=== 第99回テスト結果 ===
生徒名: 田中太郎
得点: 61点
評価: C (可)

詳細解説

1. なぜクラスを使うのか?

比較してみよう。

// クラス無し版(C言語っぽい)
string student_name = "田中太郎";
int student_score = 85;

void show_report(string name, int score, int test_number) {
  // 処理...
}

show_report(student_name, student_score, 3);

これだとデータと処理がバラバラで管理しにくい。

クラスを使うと:

// クラス版
Student student;
student.name = "田中太郎";
student.score = 85;
student.showReport(3);  // データと処理が一体化

関連するデータと処理をまとめられるのがクラスの利点だ。

2. ヘッダーファイルの書き方

#ifndef STUDENT_H_
#define STUDENT_H_

#include <string>  // string型を使うために必要

class Student {
public:                              // アクセス修飾子: パブリック(外部からアクセス可能)
  std::string name;                  // std::を明示的に書く方法
  int score;
  void showReport(int testNumber);
};

#endif // STUDENT_H_

重要ポイント

  • ヘッダーガードは#ifndef ファイル名_H_の形式
  • #include <string>でstring型を使用可能に
  • std::を明示的に書くか、using namespace std;を使うかは好み

ちなみにアクセス修飾子とあるが、これはpublic, private, protectedと存在する。 このサンプルコードではpublicとなっている。

これはアクセス修飾子と呼ばれるもので、アクセスの許可範囲を決める。

  • public: 外部から自由にアクセス可能
  • private: クラス内部からのみアクセス可能
  • protected: 継承関係でのみアクセス可能

3. スコープ解決演算子(::)

(::) <- レジ系思い出したけど、::って特になかった。

void Student::showReport(int testNumber){
  // 実装
}

Student::は「Studentクラスのメンバ関数」という意味。これがないと普通の関数として認識される。

4. メンバ変数とメンバ関数

class Student {
public:
  std::string name;    // メンバ変数(データ)
  int score;           // メンバ変数(データ)
  void showReport(...); // メンバ関数(処理)
};
  • メンバ変数: オブジェクトが持つ属性・データ
  • メンバ関数: オブジェクトができる処理・行動

5. オブジェクトの作成と操作

Student student;               // オブジェクト作成(設計図から実体を作る)
student.name = "田中太郎";      // メンバ変数に値を代入
student.showReport(3);         // メンバ関数を呼び出し

studentインスタンス(実体)と呼ばれる。同じクラスから複数のオブジェクトを作れる:

Student student1, student2;
student1.name = "田中太郎";
student2.name = "佐藤花子";

6. カプセル化

カプセル化内部の詳細を隠して、決められた方法でのみ操作できるようにすること。

カプセル化のメリットとは

  • データの整合性を保てる:変な値の代入を防げる
  • 仕様変更に強い:内部実装を変えても外部に影響しない
  • 使いやすい:複雑な処理を関数で隠せる

書き捨てコードだとほぼ不要なテクニックだが、メンテ前提のプログラムになるとそこそこ重要な要素だと思う。

以下、雑サンプルコード

hoge.score = 1094353443; // 100点満点なのに困る!
// 前提としてアクセス修飾子はprivate
// 0~100点範囲が実質保証されるので安心!
void setScore(int s) {
  if (s >= 0 && s <= 100) {
    score = s;
  } else {
    cout << "不正な点数です!" << endl;
  }
}

ちなみに、アクセス修飾子を何も書かない場合というものもある。

#include <iostream>
#include <string>
using namespace std;

class TestClass {
  string data = "class data";  // デフォルトでprivate
};

struct TestStruct {
  string data = "struct data"; // デフォルトでpublic
};

int main() {
  TestClass tc;
  TestStruct ts;
  
  // tc.data;     // ← エラー!コンパイルできない
  cout << ts.data << endl;  // ← OK!"struct data"が出力される
  
  return 0;
}

classとstructで異なるみたいだ。

より実践的な例

// 複数の学生を管理
Student students[3];

students[0].name = "田中太郎";
students[0].score = 85;

students[1].name = "佐藤花子";
students[1].score = 92;

students[2].name = "鈴木一郎";
students[2].score = 76;

// 全員の成績を表示
for (int i = 0; i < 3; i++) {
  students[i].showReport(1);
  cout << "---" << endl;
}

これで同じ種類のデータと処理をまとめて管理できる。

コンパイル方法

g++ -o main main.cpp student.cpp
./main

複数の.cppファイルを同時にコンパイルする。ヘッダーファイル(.h)は自動的にインクルードされる。

C言語との比較

C言語

typedef struct {
    char name[50];
    int score;
} Student;

void show_report(Student* student, int test_number) {
    printf("生徒名: %s\n", student->name);
    printf("得点: %d\n", student->score);
}

Student student = {"田中太郎", 85};
show_report(&student, 3);

C++

class Student {
public:
    string name;
    int score;
    void showReport(int testNumber);  // データと処理が一体化
};

Student student;
student.name = "田中太郎";
student.score = 85;
student.showReport(3);  // よりシンプル

C++の方がデータと処理の関係が明確で、使いやすい。

今日学んだこと

  • クラス: 関連するデータと処理をまとめる仕組み
  • オブジェクト: クラスから作られる実体(インスタンス
  • メンバ変数・メンバ関数: クラスが持つデータと処理
  • スコープ解決演算子: クラス名::でメンバの所属を明示
  • ヘッダーと実装の分離: 宣言と実装を分けて管理
  • アクセス修飾子: public(外部アクセス可)、private(内部のみ)、protected(継承関係)でアクセス範囲を制御
  • カプセル化: 内部の詳細を隠して決められた方法でのみ操作可能にする考え方
  • classとstructの違い: classは何も書かないとprivate、structはpublic がデフォルト
  • データの整合性: privateメンバとsetter/getter関数で不正な値の代入を防げる
  • オブジェクト指向の基本: データと処理を一体化させることで、C言語より管理しやすくなる