オブジェクト指向
今日は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言語より管理しやすくなる