クラス | Programming Place Plus Modern C++編【言語解説】 第5章

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要 🔗

この章の概要です。


オブジェクト指向プログラミング (OOP) 🔗

この章から、C++ のオブジェクト指向プログラミングに関わる機能の解説を始めていきます。この章では、オブジェクト指向における非常に重要な概念であるクラスを解説します。

クラスという用語は C++ に固有なものではなく、オブジェクト指向プログラミングの世界での共通語のようなものです。ですからまずは、オブジェクト指向プログラミングについて簡単に知っておきましょう。なお、オブジェクト指向プログラミングは、略して OOP (Object Oriented Programming) と表記されることがあります。文章を短く抑えるため、今後 OOP と書きます。

OOP に対応したプログラミング言語はいくつもあります。そのような言語は、オブジェクト指向プログラミング言語といいます。これも長いので、今後は略語(OOPL: Object Oriented Programming Language)と書きます。

OOPL はそれぞれが異なる方法で、オブジェクト指向の考え方を実現しています。そのため、ここで解説する内容は、C++ での実現方法、C++ での考え方に過ぎないことを知っておいてください。

クラスも非常に重要ですが、まずオブジェクト指向の主役であるオブジェクトから取り上げましょう。

【上級】名前のとおり、オブジェクト指向の主役はオブジェクトの方です。オブジェクト指向に対応していても、クラスがない OOPL はあり得ます。

【上級】C++ では、オブジェクト指向用語としてのオブジェクトという言葉を使うことがあるためややこしいですが、C言語でも C++ でも、ある型の値がメモリ上に置かれているとき、そのメモリ上の部分を指してオブジェクトと呼びます。規格上の「オブジェクト」の意味はこちらです。

ものすごく抽象化された表現ですが、オブジェクトとは、「データ」や「処理」をひとまとめにした「もの」です。「もの」は形ある「物体」に限定されませんし、「生き物」も「もの」です。

抽象化された考え方のまま理解することは難しいので、C++ がどのように実現しているかを理解する方が早いと思います。

C++ においては、「データ」とは変数のこと、「処理」とは関数のことです。ある1つの「もの」を表現するために必要な変数と関数をひとまとめにすれば、それがオブジェクトです。

たとえば、C言語の構造体は近い考え方をしているといえますが、関数がありません。言い換えれば、構造体に関数を加えることができれば、オブジェクトとして使えそうです。C++ での実現方法はまさにその方向性になっています。

「構造体+関数」のような存在がクラスです。「構造体+関数」がオブジェクトだとはいっていないことに注意してください。どちらの考え方でも結果的には正しいともいえますが、少し整理しておいた方が良いでしょう。

何気なく「構造体」という言葉を使うことが多いですが、実際には、構造体型という「型」と、そこから作った構造体「変数」の両面があります。それと同じで、クラスにも「型」と、そこから作った「変数」があります。単に「クラス」といったときは「型」の方を指しています。そして、そこから作った「変数」の方が「オブジェクト」です。

この関係性が整理できれば話は単純で、C++ では、クラス(型)を定義し、その型の変数を定義すれば、それがオブジェクトです。クラスには変数も関数も含めることができますから、そこから作られたオブジェクトはそれぞれが、変数と関数のセットを持っています。

構造体変数はそれぞれが、構造体型に含まれているメンバ(変数)を使えます。それと同じことです。オブジェクトはそれぞれが、クラス型に含まれているメンバ(変数と関数)が使えます。

オブジェクト指向の用語では、クラスからオブジェクトを生成することをインスタンス化(実体化)と呼びます。そして、生成されたオブジェクトのことをインスタンス(実体)と呼びます。

【上級】C++ には、テンプレートの実体化(インスタンス化)という表現もありますが(第8章)、これは別の話です。

クラスという概念が、具体的にどのようなものであるのかは、OOPL の実現方針によって異なります。とにかく、「クラスはオブジェクトを作り出す基になる存在」であるという点がポイントです。C++ では、クラスという型から、オブジェクトという変数を定義するので、この考え方に基づいているといえます。

ここから先は、OOP の考え方について取り上げていきます。OOP では、「いくつかのオブジェクトを作り」、「それぞれが自分の仕事をこなし」、「必要があれば他のオブジェクトへ仕事を依頼する」ことで、プログラムを構築するという考え方を取ります。

【上級】オブジェクト指向は、プログラミングの工程だけに限った話ではありません。小さなプログラムでは省略されますが、本来なら、プログラミングの過程の前には、分析や設計といった過程があるものです。オブジェクト指向の考え方に基づいた開発では、こういった手前の過程(上流工程)からオブジェクト指向の考え方を適用します。

OOP (「いくつかのオブジェクトを作り」の意味) 🔗

OOP は、オブジェクトを作らなければ始まりません。

オブジェクトを作るにはクラスが必要なので、まずはクラスを定義します。クラスを定義する構文は次のようになります。

class クラス名 {
    メンバ
      :
};

基本的な形は構造体と同じですが、使うキーワードが struct から class に代わります。

具体的な例を見てみましょう。「生徒」を表す Studentクラスを定義します。

class Student {
    char   name[128];   // 名前
    int    grade;       // 学年
    int    score;       // 得点
};

Studentクラスを複数のソースファイルから使うのなら、上記の定義をヘッダファイルに置けばよいですし、1つのソースファイル内でだけ使うのなら、ソースファイルに置けばよいです。

Studentクラスの定義を用意したので、あとはこの型の変数を定義すれば、それがオブジェクトです。

Student student;    // Studentクラスのオブジェクト student を生成

ここで、クラスの方が抽象的であり、オブジェクトの方が具体的であることに注目しておくと、オブジェクト指向の考え方を理解する一歩になるかもしれません。

クラス(Student)は、生徒であれば誰しもが持っているであろう要素を、変数や関数として定義しています。特定の誰かを想定していません。このような、曖昧で漠然とした状態を「抽象的である」といいます。対して、オブジェクト (student) は、特定の生徒のことを表現しており、「具体的である」といえます。

少し見方を変えるとこのようにも言えます。オブジェクトは特定の誰かを表した実体なので、それぞれの名前であったり、学年であったりというデータ(変数)を持っていて、そこには実際に「値」があります。一方、クラスの方は、変数や関数の定義はありますが、値を持っているわけではありません。いわば、枠組みだけの存在がクラスです。

【上級】C++ には、クラス単位で値を持つ変数(第23章)や、クラスに属する関数(第23章)というものも存在します。これらはよく使われるものではありますが、オブジェクト指向の考え方からすれば少々特殊な存在です。

ところで、構造体でもそうですが、自身のメンバの値が勝手に初期化されるわけではありません。構造体の場合なら、各メンバに1つ1つ値を入れて初期化しましたが、クラスでは普通はその手は取りません。クラスは関数を持てるので、初期化のための関数を作るようにします(詳細は、次章で取り上げます)。

#include <cstring>

class Student {
    char   name[128];   // 名前
    int    grade;       // 学年
    int    score;       // 得点

public:
    void SetData(const char* name, int grade, int score);
};

void Student::SetData(const char* name, int grade, int score)
{
    std::strcpy(this->name, name);
    this->grade = grade;
    this->score = score;
}

int main()
{
    Student student;
    student.SetData("Saitou Hiroyuki", 2, 80);
}

クラス定義の中に関数宣言を書きます。このような関数は、メンバ関数と呼びます。また、クラス内で宣言される変数は、メンバ変数^と呼ばれます。

public というキーワードがあります。public: のように記述することによって、その記述よりも後ろに宣言されているメンバを、オブジェクトから呼び出すことができます。

構造体のメンバを使うときと同じで、オブジェクトからメンバ関数を呼び出すには .演算子や ->演算子を使います。たとえば、SetDataメンバ関数は「student.SetData(実引数の並び)」のように呼び出すことができます。

メンバ関数の定義を書くときには、どのクラスに所属しているメンバ関数なのかを知らせる必要があるので、クラス名で修飾します。

戻り値の型 クラス名::メンバ関数名(仮引数の並び)
{
}

【上級】メンバ関数はオーバーロード第10章)できますし、インライン関数(第6章)にすることも、メンバ関数テンプレートにもできます。

クラス定義をヘッダファイルに記述する場合、一般的には、メンバ関数の定義をソースファイル側に書きます。

【上級】ただし、メンバ関数がインライン関数であったり、メンバ関数テンプレートであったりする場合には、定義もヘッダファイル側に記述します。

SetDataメンバ関数の定義内には、「this->name」という記述があります。this は、メンバ関数の中で自動的に作られるポインタで、thisポインタと呼ばれます。thisポインタは、そのメンバ関数を呼び出す元となったオブジェクトを指しています。

たとえば、「student1.SetData()」のように呼び出したときは、this は student1 を指していますし、「student2.SetData()」なら、this は student2 を指しています。「student1->SetData()」なら、this は student1 を指しています。

したがって、「this->name」は、メンバ変数の name のことを指しています。this を使わずに、「name = name;」と書いてしまうと、仮引数のname へ仮引数の name を代入することになってしまうので、区別を付けるために、this が必要です。

仮引数とメンバ変数の区別を付けるには、this を使う以外にも、名前の方をずらす手もあります。よく使われる方法は、メンバ変数の名前にはつねに「m」や「_」のような目印を付けることです。たとえば、「mName」「m_name」「name_」といった感じです。

「_name」のように、頭にアンダーバーの付いた名前は、処理系が定義する名前と被る可能性があるので、良い方法ではありません(C言語編第3章参照)。

今後は、メンバ変数の頭に「m」を付ける方法を使います。

#include <cstring>

class Student {
    char   mName[128];   // 名前
    int    mGrade;       // 学年
    int    mScore;       // 得点

public:
    void SetData(const char* name, int grade, int score);
};

void Student::SetData(const char* name, int grade, int score)
{
    std::strcpy(mName, name);
    mGrade = grade;
    mScore = score;
}

int main()
{
    Student student;
    student.SetData("Saitou Hiroyuki", 2, 80);
}

OOP (「それぞれが自分の仕事をこなし」の意味) 🔗

「それぞれが自分の仕事をこなし」という意味は、たとえば、Studentクラスのオブジェクトは、名前・学年・得点を管理していますが、それ以外のことには関知しないということです。

たとえば、生徒が進級して学年が1つ上がることは、生徒自身の範疇です。そのため、Studentクラスが、進級させるメンバ関数 Promotion を持つことは間違っていません。

#include <cassert>
#include <cstring>

class Student {
    char   mName[128];   // 名前
    int    mGrade;       // 学年
    int    mScore;       // 得点

public:
    void SetData(const char* name, int grade, int score);

    void Promotion();
};

void Student::SetData(const char* name, int grade, int score)
{
    std::strcpy(mName, name);
    mGrade = grade;
    mScore = score;
}

void Promotion()
{
    assert(mGrade < 6);  // 6学年まで

    mGrade++;
}

int main()
{
    Student student;
    student.SetData("Saitou Hiroyuki", 2, 80);

    student.Promotion();
}

一方、その生徒を受け持っている先生の年齢には無関心であるのが自然です。

class Student {
    char   mName[128];   // 名前
    int    mGrade;       // 学年
    int    mScore;       // 得点

    // 不適切
    int   mMyTeacherAge;  // 先生の年齢

public:
    void SetData(const char* name, int grade, int score);
};

先生の年齢は、「先生」を表す Teacherクラスが別にあって、そちらが管理するべきものです。この考え方は、自分にとって必要なこと、すべきことにだけ集中しているといえます。

【上級】あるコードの部分が、ある責任を果たすことにどれだけ集中しているかを表す、凝集度(ぎょうしゅうど)という尺度があります。良い設計は、凝集度が高くなるといわれています。

OOP (「必要があれば他のオブジェクトへ仕事を依頼する」の意味) 🔗

最後に、「必要があれば他のオブジェクトへ仕事を依頼する」という部分です。

先ほど、生徒が、先生の年齢を持つべきではないという話が出ました。代わりに、先生のオブジェクトを作り、生徒はそのオブジェクトを保持すればよいです。

#include <cstring>

class Teacher {
    char  mName[128];   // 名前
    int   mAge;         // 年齢

public:
    void SetData(const char* name, int age);
};

void Teacher::SetData(const char* name, int age)
{
    std::strcpy(mName, name);
    mAge = age;
}

class Student {
    char   mName[128];   // 名前
    int    mGrade;       // 学年
    int    mScore;       // 得点

    const Teacher* mMyTeacher;  // 自身を担当している先生

public:
    void SetData(const char* name, int grade, int score, const Teacher* teacher);
};

void Student::SetData(const char* name, int grade, int score, const Teacher* teacher)
{
    std::strcpy(mName, name);
    mGrade = grade;
    mScore = score;
    mMyTeacher = teacher;
}

int main()
{
    Teacher ikeda;
    ikeda.SetData("Ikeda Naohiro", 41);

    Student student;
    student.SetData("Saitou Hiroyuki", 2, 80, &ikeda);
}

ここでは、Teacherクラスのオブジェクト(ikeda)を指すポインタを保持する形にしました。

このような設計にしておけば、ikeda先生が誕生日を迎えて年齢を加算するときには、ikeda オブジェクトのメンバ関数を使って加算させればよいです。

#include <cstring>

class Teacher {
    char  mName[128];   // 名前
    int   mAge;         // 年齢

public:
    void SetData(const char* name, int age);

    // 年齢を加算する
    void IncAge();
};

void Teacher::SetData(const char* name, int age)
{
    std::strcpy(mName, name);
    mAge = age;
}

void Teacher::IncAge()
{
    mAge++;
}

class Student {
    char   mName[128];   // 名前
    int    mGrade;       // 学年
    int    mScore;       // 得点

    const Teacher* mMyTeacher;  // 自身を担当している先生

public:
    void SetData(const char* name, int grade, int score, const Teacher* teacher);
};

void Student::SetData(const char* name, int grade, int score, const Teacher* teacher)
{
    std::strcpy(mName, name);
    mGrade = grade;
    mScore = score;
    mMyTeacher = teacher;
}

int main()
{
    Teacher ikeda;
    ikeda.SetData("Ikeda Naohiro", 41);

    Student student;
    student.SetData("Saitou Hiroyuki", 2, 80, &ikeda);

    ikeda.IncAge();
}

「ikeda.IncAge()」を1回呼び出せば、すべての生徒オブジェクトが持つ情報を書き換えてまわる必要はありません。これがあるべき姿です。

もし、Studentクラスが mMyTeacherAge を持つ設計だったら、ikeda先生の年齢を加算をどう実現すればいいでしょうか? Studentオブジェクトは生徒の人数分あるので、そのすべてを調べる必要があります。このとき、担当している先生が ikeda先生であることを確認できなければなりません。また、一部の生徒の情報を変更し損なうと、生徒ごとに ikeda先生の年齢の認識が食い違う状況ができてしまいます。
このように、設計は複雑化し、処理の量は増大し、危険性も生まれます。自身が管理すべきことと、そうでないことの切り分け、役割分担が非常に重要です。

ところで、メンバ変数の値がきちんと意図したとおりになっているのかどうか気になります。たとえば、自身のメンバ変数の値を出力するメンバ関数を定義するという手があります。

#include <cstring>
#include <iostream>

class Teacher {
    char  mName[128];   // 名前
    int   mAge;         // 年齢

public:
    void SetData(const char* name, int age);
    void IncAge();
    void Print();
};

void Teacher::SetData(const char* name, int age)
{
    std::strcpy(mName, name);
    mAge = age;
}

void Teacher::IncAge()
{
    mAge++;
}

void Teacher::Print()
{
    std::cout << "mName: " << mName << "\n"
              << "mAge: " << mAge << std::endl;
}


class Student {
    char   mName[128];   // 名前
    int    mGrade;       // 学年
    int    mScore;       // 得点

    const Teacher* mMyTeacher;  // 自身を担当している先生

public:
    void SetData(const char* name, int grade, int score, const Teacher* teacher);
    void Print();
};

void Student::SetData(const char* name, int grade, int score, const Teacher* teacher)
{
    std::strcpy(mName, name);
    mGrade = grade;
    mScore = score;
    mMyTeacher = teacher;
}

void Student::Print()
{
    std::cout << "mName: " << mName << "\n"
              << "mGrade: " << mGrade << "\n"
              << "mScore: " << mScore << "\n"
              << "mMyTeacher: " << mMyTeacher << std::endl;
}

int main()
{
    Teacher ikeda;
    ikeda.SetData("Ikeda Naohiro", 41);
    ikeda.Print();

    std::cout << "-----" << std::endl;

    Student student;
    student.SetData("Saitou Hiroyuki", 2, 80, &ikeda);
    student.Print();

    std::cout << "-----" << std::endl;

    ikeda.IncAge();
    ikeda.Print();
}

実行結果:

mName: Ikeda Naohiro
mAge: 41
-----
mName: Saitou Hiroyuki
mGrade: 2
mScore: 80
mMyTeacher: 008FFCC8
-----
mName: Ikeda Naohiro
mAge: 42

【上級】出力のためのメンバ関数を呼び出すよりも、「std::cout << ikeda;」のような使い方ができる方が便利で自然です。std::cout にとっては Teacherクラスは未知の存在なので、そのままでは不可能ですが、対応させる方法はあります。第35章で取り上げます。


練習問題 🔗

問題① Student型の配列を作り、3人の生徒を管理できることを確認してください。

問題② Studentクラスに、名前、学年、得点を返すメンバ関数を追加してください。そして、動作確認のため、問題①の3人の生徒の情報を出力するプログラムを書いてみてください。

問題③ 「先生」を表現する Teacherクラスが、受け持っている「生徒」を管理したいとします。1人の「先生」が、最大で30人の「生徒」を受け持つとしたら、Teacherクラスをどう書けば良いでしょうか?


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 C++編【言語解説】第11章「クラス」の修正に合わせて、内容更新。

 新規作成。



前の章へ (第4章 名前空間)

次の章へ (第6章 アクセス指定子)

Modern C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る