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

トップページModern C++編 C++編](../../index.html) – 第5章

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

問題①

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


クラスは型に過ぎないので、int a[3]; のように配列を作れるように、Student a[3]; のように配列にできます。

#include <cstring>
#include <iostream>

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(this->mName, name);
    mGrade = grade;
    mScore = score;
}

int main()
{
    Student students[3];
    students[0].SetData("Saitou Takashi", 2, 80);
    students[1].SetData("Yamamoto Yuko", 1, 77);
    students[2].SetData("Ueda Kouji", 3, 74);
}

問題②

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


たとえば、次のようになります。

// Student.h
#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

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

public:
    void SetData(const char* name, int grade, int score);
    
    const char* GetName();
    int GetGrade();
    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;
}

const char* Student::GetName()
{
    return mName;
}

int Student::GetGrade()
{
    return mGrade;
}

int Student::GetScore()
{
    return mScore;
}
// main.cpp
#include "Student.h"
#include "Teacher.h"

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

int main()
{
    Student students[3];
    students[0].SetData("Saitou Takashi", 2, 80);
    students[1].SetData("Yamamoto Yuko", 1, 77);
    students[2].SetData("Ueda Kouji", 3, 74);

    for (int i = 0; i < SIZE_OF_ARRAY(students); ++i) {
        std::cout << students[i].GetName() << "\n"
                  << students[i].GetGrade() << "\n"
                  << students[i].GetScore() << std::endl;
    }
}

実行結果:

Saitou Takashi
2
80

Yamamoto Yuko
1
77

Ueda Kouji
3
74

メンバ変数ごとに専用のメンバ関数^を用意しました。1つのメンバ関数でまとめて返す形もあり得ますが、セットになっていることに必然性がないものは、1つ1つ返す関数を作った方が良い場合が多いです。

セットになっている必然性がある情報は、それをまとめた構造体やクラスで返します。たとえば、「学年」に加えて「組」を管理しているなら、恐らく「2年3組」のような形で返さないと意味を成さないでしょうから、セットになっている方が自然です。

なお、for文のところは、範囲for文を使うのも良いのですが、次のように書くと1回1回コピーが作られるので、効率が良くないことに注意してください。

int main()
{
    Student students[3];
    students[0].SetData("Saitou Takashi", 2, 80);
    students[1].SetData("Yamamoto Yuko", 1, 77);
    students[2].SetData("Ueda Kouji", 3, 74);

    for (Student s : students) {
        std::cout << s.GetName() << "\n"
                  << s.GetGrade() << "\n"
                  << s.GetScore() << std::endl;
    }
}

【上級】効率を良くするためには、参照(第12章)を使って、「for (Student& s : students)」のように書くと良いです。

問題③

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


メンバ変数としては、Student*型で要素数 30 の配列があれば良いでしょう。このとき、Student型ではなく、ポインタとして扱った方がいろいろと利点が生まれます。

1つには、生徒のオブジェクトは Teacherクラスとは別のところに作られているので、実体にすると、コピーを作ることになり、メモリ的にも処理効率的にも無駄があります。

また、Student のオブジェクトのメンバ変数が、Teacherクラスの知らないところで更新されるかもしれません。実体で持った場合、コピーを作ったタイミングでの情報しか保持できないため、どこかで行われた更新に追従できません。たとえば、知らないうちに「学年」が上がっているかもしれません。

配列の要素数の 30 については、どこかで名前の付いた定数を定義しておきたいところです。ここでは、クラス定義の中に、enum で定義することにします。

【上級】実際には、クラス内で使う定数が欲しいときは、staticメンバ定数(第23章)にすることが多いですが、整数の定数であれば、enum で定義することもあります。

// Teacher.h
#ifndef TEACHER_H_INCLUDED
#define TEACHER_H_INCLUDED

#include "Student.h"

// 先生クラス
class Teacher {
    enum {
        STUDENT_MAX = 30,
    };

    Student*     mStudents[STUDENT_MAX];
};

#endif

これだけでは何なので、動作確認のためにも、他のメンバを考えてみましょう。

まず、受け持つ生徒たちをメンバ変数の配列に入れていかないといけません。たとえば、次のようなメンバ関数を用意します。

void AddStudent(Student* student);

この関数の実装を書くためには、配列がどこまで使用済みか(生徒が登録済みか)が分からないといけません。そこでメンバ変数を追加して、受け持ち済みの生徒の人数を管理するようにします。まとめると、次のようになります。

// Teacher.h
#ifndef TEACHER_H_INCLUDED
#define TEACHER_H_INCLUDED

#include "Student.h"

// 先生クラス
class Teacher {
    enum {
        STUDENT_MAX = 30,
    };

    Student*     mStudents[STUDENT_MAX];
    int          mStudentNum;
    
public:
    void AddStudent(Student* student);
};

#endif
// Teacher.cpp
#include "Teacher.h"
#include <cassert>

void Teacher::AddStudent(Student* student)
{
    assert(student != nullptr);
    assert(mStudentNum < STUDENT_MAX);

    mStudents[mStudentNum] = student;
    mStudentNum++;
}

assert(あるいは適切な処置)をきちんと書くようにしましょう。ここでは、ヌルポインタが渡されてくる可能性と、配列がすでに一杯になっている可能性をチェックしています。

これで良さそうに見えますが、mStudentNum は未初期化な状態で始まっているので、うまくいきません。メンバ変数に初期値を与える方法はいろいろありますが、現時点の知識でできることは、初期化用のメンバ関数を追加することです。

【上級】他の方法として、もっとも一般的なのは、コンストラクタを使う方法です。あるいは、メンバ変数を宣言する際に初期値を与えられます(int mStudentNum = 0;)。この書き方は自然に思えますし、言われずとも試した人がいるかもしれませんが、実は C++11 になるまで、この書き方はできませんでした。いずれの方法も、第7章で解説します。

// Teacher.h(一部省略)
class Teacher {
    enum {
        STUDENT_MAX = 30,
    };

    Student*     mStudents[STUDENT_MAX];
    int          mStudentNum;
    
public:
    void Initialize();
    void AddStudent(Student* student);
};

#endif
// Teacher.cpp(一部省略)
#include "Teacher.h"

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

void Teacher::Initialize()
{
    for (int i = 0; i < SIZE_OF_ARRAY(mStudents); ++i) {
        mStudents[i] = nullptr;
    }
    mStudentNum = 0;
}

Teacherクラスをインスタンス化した直後に Initializeメンバ関数を呼び出すことで、メンバ変数が初期状態になります。mStudents の方の初期化は、実装によっては不要かもしれませんが、基本的に不定な状態は避けておいた方が無難なので、きちんと初期化しておきます。

あとは、登録済みの生徒を確認できるように、自分が担当している生徒の名前を一覧表示する PrintMyStudentsNamesメンバ関数を追加します。

// Teacher.h(一部省略)
class Teacher {
    enum {
        STUDENT_MAX = 30,
    };

    Student*     mStudents[STUDENT_MAX];
    int          mStudentNum;
    
public:
    void Initialize();
    void AddStudent(Student* student);
    void PrintMyStudentsNames();
};

#endif
// Teacher.cpp(一部省略)
#include "Teacher.h"
#include <iostream>

void Teacher::PrintMyStudentsNames()
{
    for (int i = 0; i < mStudentNum; ++i) {
        std::cout << mStudents[i]->GetName() << std::endl;
    }
}

このぐらいで良いでしょう。main関数も合わせると、最終形は次のようになります。

// Student.h
#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

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

public:
    void SetData(const char* name, int grade, int score);
    
    const char* GetName();
    int GetGrade();
    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;
}

const char* Student::GetName()
{
    return mName;
}

int Student::GetGrade()
{
    return mGrade;
}

int Student::GetScore()
{
    return mScore;
}
#ifndef TEACHER_H_INCLUDED
#define TEACHER_H_INCLUDED

#include "Student.h"

// 先生クラス
class Teacher {
    enum {
        STUDENT_MAX = 30,  // 受け持ち可能な生徒の最大人数
    };

    Student*     mStudents[STUDENT_MAX];  // 受け持っている生徒
    int          mStudentNum;             // 受け持っている生徒の人数
    
public:
    void Initialize();                    // 初期化
    void AddStudent(Student* student);    // 生徒を追加
    void PrintMyStudentsNames();          // 生徒全員の名前を出力
};

#endif
// Teacher.cpp
#include "Teacher.h"
#include <cassert>
#include <iostream>

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

void Teacher::Initialize()
{
    for (int i = 0; i < SIZE_OF_ARRAY(mStudents); ++i) {
        mStudents[i] = nullptr;
    }
    mStudentNum = 0;
}

void Teacher::AddStudent(Student* student)
{
    assert(student != nullptr);
    assert(mStudentNum < STUDENT_MAX);

    mStudents[mStudentNum] = student;
    mStudentNum++;
}

void Teacher::PrintMyStudentsNames()
{
    for (int i = 0; i < mStudentNum; ++i) {
        std::cout << mStudents[i]->GetName() << std::endl;
    }
}
// main.cpp
#include "Student.h"
#include "Teacher.h"

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

int main()
{
    Student students[3];
    students[0].SetData("Saitou Takashi", 2, 80);
    students[1].SetData("Yamamoto Yuko", 1, 77);
    students[2].SetData("Ueda Kouji", 3, 74);

    Teacher teacher;
    teacher.Initialize();
    for (int i = 0; i < SIZE_OF_ARRAY(students); ++i) {
        teacher.AddStudent(&students[i]);
    }
    teacher.PrintMyStudentsNames();
}

実行結果:

Saitou Takashi
Yamamoto Yuko
Ueda Kouji

長くなりましたが、複数のクラスを組み合わせてプログラムが形作られる様を見てきました。この形が最良であるということではありません。1つの形であると思ってください。作ろうとしているプログラムが求める要件に、適切に応えられる形を模索する必要があります。



参考リンク


更新履歴

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

’2018/7/13 サイト全体で表記を統一(「静的メンバ」–>「staticメンバ」)

’2017/7/11 新規作成。



第5章のメインページへ

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

Programming Place Plus のトップページへ



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