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

先頭へ戻る

この章の概要

この章の概要です。

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

この章では、オブジェクト指向プログラミングに関わる重要な概念であるクラスを解説します。

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

OOP での主役はオブジェクトです。
この章のテーマであるクラスというのは、オブジェクトの型のことです。 ここで「型」というのは、int型とか double型とか、MyData型(構造体)とかいうときの「型」です。 C++ には、クラス(クラス型)というものがあるという訳です。

OOP では、「複数のオブジェクトを用意し」、「それぞれが自分の仕事をこなし」、「必要があれば他のオブジェクトへ仕事を依頼する」ことで、 プログラムを構築するという考え方を取ります。 ここから先では、この3つの文の意味を順番に追ってみます。

OOP (「複数のオブジェクトを用意し」の意味)

先ほどの OOP の説明で、「複数のオブジェクトを用意し」というのがありました。 これは、C++ のソースコードの中で、クラスを定義し、そのクラス型の変数を定義するということです。 具体的に書くと、次のようになります。

// 生徒クラス
class Student {
    char  name[32];   // 名前
    int   grade;      // 学年
    int   score;      // 得点
};

int main()
{
    Student student1;  // オブジェクトを用意する(=クラス型の変数を定義する)
    Student student2;  // オブジェクトを用意する(=クラス型の変数を定義する)
}

この場合、クラス型の変数を2つ定義したので、2つのオブジェクトが用意されたということです。

クラスを定義するには、classキーワードを使います。 構文はほとんど構造体と同じに見えると思いますが、実際、ここで定義した Studentクラスの場合は同じと考えて構いません。 違いは、次章で取り上げます。
なお、オブジェクトを用意することを、(クラスを)インスタンス化すると言います。 また、オブジェクトのことをインスタンス(実体)と呼ぶこともあります。

このように、C++ のクラスという概念は、単なる型に過ぎません。 int型や char型、double型といった、最初から用意されている型を組み合わせて、独自に都合の良い型を作り出せます。 この考え方は、C言語の構造体でも同じです。

さて、定義された2つのオブジェクトは、別々のオブジェクトなので、メンバも別々のものになります。 クラスのメンバに値を与えて確認してみます。 そのためには、値を設定するための関数を、クラス定義の中に追加して、これを呼び出します。

#include <cstring>

// 生徒クラス
class Student {
    char  name[32];   // 名前
    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 student1;
    student1.SetData("Saitou Takashi", 2, 80);
    
    Student student2;
    student2.SetData("Yamamoto Yuko", 1, 77);
}

関数のプロトタイプ宣言を書くときの感覚で、クラス定義の中に宣言を書きます。 このようにして作られる関数を、メンバ関数と呼びます。 また、クラス内に含まれる変数はメンバ変数と呼ばれます。
クラス定義の中に、typedef による型名の定義や、列挙型の定義などを入れることもできます。

なお、「public:」という記述が追加されています。 これについては次章で詳細を説明しますが、 これがあることで、「student1.SetData()」のように、 オブジェクトから、そのメンバ関数をメンバ関数を呼び出すことができるようになります。
オブジェクトからメンバ関数を呼び出す方法は、構造体のメンバを使うときと同じで、 .演算子を使うか、ポインタ経由なら ->演算子を使います。

メンバ関数の定義の方ですが、「Student::SetData」のように、名前をクラス名で修飾します。 今後、説明文中でも「Student::SetData()」のように表記します。
関数内には、「this->name」という記述がありますが、 thisキーワードは、このメンバ関数を呼び出す元となったオブジェクト(を指すポインタ)のことを意味しています。 「student1.SetData()」のように呼び出したときは、this は &student1 のことになるし、 「student2.SetData()」なら、this は &student2 です。 もちろん、「student1->SetData()」なら、this は student1 のことです。
従って「this->name」というのは、メンバ変数の name のことを指しています。 this を使わずに、「name = name;」と書いてしまうと、仮引数のname へ仮引数の name を代入することになってしまうので、 区別を付けるために、this が必要です。

「name = name;」のような記述になってしまうことを防ぐためには、 this を使う以外にも、メンバ変数の名前に「m」や「_」を付けるように徹底するという方法もよく使われています。 例えば、「mName」「m_name」「name_」といった感じです。 ちなみに「_name」のように、頭にアンダーバーの付いた名前は、処理系が定義する名前と被る可能性があるので、 良い方法ではありません(C言語編第3章参照)。
当サイトでは今後、メンバ変数の頭に「m」を付ける方法を採用します。

なお、クラス定義はヘッダファイル、メンバ関数の定義はソースファイル側に記述するのが一般的です

メンバ関数がインライン関数(第6章)であったり、関数テンプレート第9章であったりする場合には、定義もヘッダファイル側に記述します

// Student.h
#ifndef STUDENT_H
#define STUDENT_H

// 生徒クラス
class Student {
    char  name[32];   // 名前
    int   grade;      // 学年
    int   score;      // 得点

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

#endif
// Student.cpp
#include "Student.h"
#include <cstring>

void Student::SetData(const char* name, int grade, int score)
{
    std::strcpy(this->name, name);
    this->grade = grade;
    this->score = score;
}
// main.cpp
#include "Student.h"

int main()
{
    Student student1;
    student1.SetData("Saitou Takashi", 2, 80);
    
    Student student2;
    student2.SetData("Yamamoto Yuko", 1, 77);
}

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

次に、「それぞれが自分の仕事をこなし」という部分です。 Studentクラスの場合、生徒の名前・学年・得点を管理することが仕事であり、 それ以外のことには一切関知しない(ようにするべきだ)ということです。

例えば、生徒の得点に応じて、ABCのランク分けをしたいとします。 次の例のように、Studentクラスに、ランクを判定するメンバ関数を追加することは可能です。

// Student.h
#ifndef STUDENT_H
#define STUDENT_H

// 生徒クラス
class Student {
    char  mName[32];   // 名前
    int   mGrade;      // 学年
    int   mScore;      // 得点

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

#endif
// Student.cpp
#include "Student.h"
#include <cstring>

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

char Student::GetRank()
{
    if (mScore >= 80) {
        return 'A';
    }
    else if (mScore >= 50) {
        return 'B';
    }
    return 'C';
}
// main.cpp
#include "Student.h"
#include <iostream>

int main()
{
    Student student1;
    student1.SetData("Saitou Takashi", 2, 80);
    std::cout << student1.GetRank() << std::endl;
    
    Student student2;
    student2.SetData("Yamamoto Yuko", 1, 77);
    std::cout << student2.GetRank() << std::endl;
}

実行結果:

A
B

しかし、恐らく一般的には、生徒のランク付けを行う仕事は生徒自身のものでは無いでしょう。 例えば、先生が生徒をランク付けするのだとするならば、先生を表す Teacharクラスが定義され、 そちらに、ランクを判定するメンバ関数がある方が、仕事の分担を正確に表現できます。

// Student.h
#ifndef STUDENT_H
#define STUDENT_H

// 生徒クラス
class Student {
    char  mName[32];   // 名前
    int   mGrade;      // 学年
    int   mScore;      // 得点

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

#endif
// Student.cpp
#include "Student.h"
#include <cstring>

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

int Student::GetScore()
{
    return mScore;
}
// Teacher.h
#ifndef TEACHER_H
#define TEACHER_H

#include "Student.h"

// 先生クラス
class Teacher {
public:
    char Ranking(Student* student);
};

#endif
// Teacher.cpp
#include "Teacher.h"

char Teacher::Ranking(Student* student)
{
    const int score = student->GetScore();

    if (score >= 80) {
        return 'A';
    }
    else if (score >= 50) {
        return 'B';
    }
    return 'C';
}
// main.cpp
#include "Student.h"
#include "Teacher.h"

int main()
{
    Student student1;
    student1.SetData("Saitou Takashi", 2, 80);
    
    Student student2;
    student2.SetData("Yamamoto Yuko", 1, 77);

    Teacher teacher;
    std::cout << teacher.Ranking(&student1) << std::endl;
    std::cout << teacher.Ranking(&student2) << std::endl;
}

実行結果:

A
B

Teacher::Ranking() がランク付けを行うメンバ関数です。 Teacherクラスのメンバ関数から、Studentクラスのメンバを使うためには、Studentクラスのオブジェクトが必要です。 これは、メンバがオブジェクトごとに存在しているものだから、「誰の」かを示さなければならないためです。 そのため、Teacher::Ranking() に、Studentクラスのオブジェクトを指すポインタを渡すようにしています。 ポインタを使っているのは、C言語で構造体をそのまま渡すと効率が悪いために、 ポインタを使うのと同じ考え方です。

Teacher::Ranking() は、Studentクラスのオブジェクトから、その生徒の得点を知る必要があります。他のクラスのメンバ変数の値を知りたいときには、そのためのメンバ関数を用意するのが普通です。ここでは、Student::GetRank() を用意しました。このようにわざわざメンバ関数を経由させる理由については、次章で取り上げます。

実のところ、Teacher::Ranking() の中で、「student->mRank」としてもコンパイルできません。これは、mRank が「public:」のところに属していないためです。ただし、メンバ変数は「public:」に属すようにするべきではなく、ここで挙げた例のように、メンバ関数を追加して対応する方が適切です。

このように、クラスの担当範囲というものをしっかりと意識して、 それを逸脱しないように気を付けて下さい。 また、こうやって仕事を分担することで、多人数のプログラマが共同作業する場合に、 クラスごとにプログラミング作業を分担させることで、現実の仕事の分担もやりやすくなります。

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

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

ランク付けを行った結果を、標準出力に書き出していますが、 この書き出すという処理は、Teacher自身が行っておらず、std::cout に任せています。 実は「std::cout」もオブジェクトなので、これは他のオブジェクトへ仕事を依頼していると言えます。

ここでは、得点によってランク付けすることまでが先生(teacher) の仕事であって、 出力は仕事の範囲外だという考え方をしています。
これが正しい方法であると言っているのではなく、そういう方針にしたということです。 仕事をどう分担するかを検討することは、プログラマの仕事です。

クラス

ここまで説明してきたように、C++ における OOP ではクラスが重要な役割を持っています。 プログラミング言語の種類によって、クラスの意味合いには多少の違いがありますが、C++ においては単なる型です。

C++プログラマは、int型、char型といった既存の型を自由に組み合わせて、独自のクラスを定義できます。
これだけならC言語の構造体でも同じことですが、構造体と違って、クラスはメンバ関数を持つことができます。 このおかげで、必要となる機能もワンセットにして定義でき、部品としての独立性を増すことができます。 また、余計な機能を付けないようにすることや、 不必要に機能を公開しないようにすることで(public: にしたメンバだけが公開される)、 堅牢性を増すこともできます。

ちなみに、メンバ変数だけしか無いようなら構造体にするか、他のクラスへ移すのが適切であることが多く、 メンバ関数だけしか無いようなら、通常の関数にしたり(必要に応じて、名前空間に含める)、他のクラスへ移すのが適切かも知れません。

何をクラスにすればいいのかという問題には、さまざまな考え方があります。 「オブジェクト=もの」なので、1つの「もの」を1つの「クラス」とするのが基本であるだとか、 名詞として表現されるものを「クラス」にするのが良いなどと言われることがあります (この場合、動詞として表現されることをメンバ関数にします)。
重要なのは「1つのクラスに機能を詰め込みすぎない」ことです。 とりあえず、これだけでも守るようにしてみて下さい。 例えば、「生徒」「先生」「学校」といった要素を、1つのクラスだけで賄おうとすることは、大抵誤った設計です。


練習問題

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

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

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


解答ページはこちら

参考リンク



更新履歴

'2017/7/11 新規作成。



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

次の章へ(第6章 カプセル化)

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS