Modern C++編【言語解説】 第6章 カプセル化

先頭へ戻る

この章の概要

この章の概要です。

カプセル化

クラスが持つ機能の1つに、アクセス指定子があります。 これは既に、第5章の Studentクラスの例の中で登場していて、「public:」という記述がそれです。

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

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

publicキーワードは、クラスか構造体の定義の中で使用できます。 「public:」という記述によって、それ以降に書かれたメンバが「公開」されます

公開」というのは、そのクラスのオブジェクトを使って、 「student.Print();」とか「student->Print();」のように、メンバを使うことができるということです。 クラス定義の外部から使える(見える)ということで「公開」と表現されます。

先ほどの例では、「公開」されていないメンバ変数が3つありました。 「公開」されていないので、「student.mGrade = 2;」とか「student->mScore = 100;」のような使い方ができません。 このような状態を「非公開」であると言います。

クラスの場合、デフォルトで「非公開」であるとみなされますから、 「公開」したいメンバは、「public:」よりも後ろに記述しなければなりません。 なお、明示的に「非公開」であることを表したい場合は、privateキーワードを使います。

class Student {
private:  // 以下のメンバは「非公開」
    char  mName[32];   // 名前
    int   mGrade;      // 学年
    int   mScore;      // 得点

public:  // 以下のメンバは「公開」
    void SetData(const char* name, int grade, int score);
    char GetRank();
};

アクセス指定子には、public、private の他にもう1つ、protected があります。これは、第33章で解説します。

実際のプログラムで、「公開」と「非公開」の違いを確認しておきましょう。

class Test {
public:
    int mPublicValue;
    void SetValue(int value);

private:
    int mPrivateValue;
};

void Test::SetValue(int value)
{
    mPrivateValue = value;
}

int main()
{
    Test test;
    test.mPublicValue = 100;
    test.mPrivateValue = 200;  // コンパイルエラー
    test.SetValue(200);
}

「公開」されている mPublicValue へはアクセスできますが、「非公開」の mPrivateValue にはアクセスできず、コンパイルエラーとなります。 一方、SetValueメンバ関数のようなメンバ関数の内部からは、「公開」「非公開」問わず、アクセスできます。


ちなみに、好みの問題でもありますが、「公開」のメンバを先頭側に集めて、「非公開」のメンバを後ろに集めることが多いです。 これは、クラスを利用する人の立場で見れば、「非公開」のメンバには興味が無いからです(呼び出せないので)。 当サイトでも、この考え方を取って、次のような順番で書くことにします。

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

private:
    char  mName[32];   // 名前
    int   mGrade;      // 学年
    int   mScore;      // 得点
};

また、public や private が何度も登場することは問題ありません。 そのため、極端ではありますが、次のように書くこともできます。

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

private: char  mName[32];   // 名前

public: char GetRank();
	
private: int  mGrade;  // 学年
private: int  mScore;  // 得点
};

勿論、こんな分かりづらい書き方は避けましょう。


メンバを「非公開」にする機能をうまく使うことは、OOP では非常に重要なことです。 例えば、メンバ変数score が「公開」されていると、「student.score = 500;」のように書けてしまいます。 もし、得点として正常な値が、0~100 であるとすれば、この代入は不正です。 クラスの外部から、メンバ変数score を書き換える手段が、SetDataメンバ関数経由だけに限定されていれば、

void Student::SetData(const char* name, int grade, int score);
{
    assert(0 <= score && score <= 100);

    std::strcpy(mName, name);
    mGrade = grade;
    mScore = score;
}

このようにチェックを入れることができます。 「あらかじめチェックを入れておくことができる」とも言えますが、「後から追加できる」ということも重要です。 「student.mScore = 100;」のような代入を至る所でされてしまった後では、このような追加は難しいです。

なお、メンバの一部を「非公開」にすることによって、データや処理内容、型といった各種要素を外部から見えないようにすることを、 OOP の用語で、カプセル化と言います。 カプセル化は、OOP の特徴の中でも、特筆すべき重要な考え方です。

setter と getter

何を「公開」して、何を「非公開」にすればいいかですが、 基本方針としては、クラスのメンバ変数は絶対に「非公開」にすることです。 とりあえず、この基本方針だけは守り通してみて下さい。

もし、「student.score = 100;」のように、メンバ変数への代入が必要なら、そのための「公開」のメンバ関数を用意します。 このような、メンバ変数の値をセットするためのメンバ関数は、セッター(setter) と呼ばれます。
また、「score = student.score;」のように、メンバ変数の値を取得したい場合は、やはりそのための「公開」のメンバ関数を用意します。 このような、メンバ変数の値を返すためのメンバ関数は、ゲッター(getter) と呼ばれます。

具体的には、次のようになります。

class Student {
public:
    void SetScore(int score);
    int GetScore();

private:
    int          mScore;  // 得点
};
void Student::SetScore(int score)
{
    assert(0 <= score && score <= 100);
    mScore = score;
}

int Student::GetScore()
{
    return mScore;
}

setter と getter は単なる呼び名に過ぎず、正体は普通のメンバ関数ですから、セットで作らないといけない訳ではありません。 むしろ、必要性の無いものは作らないことが重要です。
また、メンバ関数の名前としては、setter は「Set~」とすることが多く、 getter は「Get~」とするか単に「Score」のような名前にすることが多いです。

constメンバ関数

getter となるメンバ関数の実装は普通、メンバ変数の値を return で返すだけのはずです。 つまり、メンバ変数の値を書き換えることが無い訳ですが、このことを明示しておくと、 より良い、getter を作れます。 そのためには、次のように、メンバ関数の宣言の末尾に、const修飾子を付加します。

class Student {
public:
    int GetScore() const;
};
int Student::GetScore() const
{
    return mScore;
}

メンバ関数の宣言の末尾に「const」と書くと、constメンバ関数という特殊なメンバ関数になります。 constメンバ関数内で、メンバ変数を書き換えるようなコードを書くと、コンパイルエラーになります。

あるオブジェクトが const指定されている場合、 そのオブジェクトの状態(=メンバ変数)を書き換えることはできません。 例えば、constメンバ関数でないメンバ関数(以降、非constメンバ関数)は、 メンバ変数を書き換えている可能性があるため、const なオブジェクトからは呼び出すことができません。 しかし、constメンバ関数は、メンバ変数の書き換えを禁止されているため、 constオブジェクトから呼び出せるようになっています。
constポインタ経由で、オブジェクトのメンバを呼び出す場合も同様です。

// Student.h
#ifndef STUDENT_H
#define STUDENT_H

// 生徒クラス
class Student {
public:
    void SetData(const char* name, int grade, int score);
    int GetScore() const;
    
private:
    char  mName[32];   // 名前
    int   mGrade;      // 学年
    int   mScore;      // 得点
};

#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() const
{
    return mScore;
}
// main.cpp
#include <iostream>
#include "student.h"

int main()
{
    Student s1;
    const Student s2;
    Student* p1 = &s1;
    const Student* p2 = &s1;

    s1.SetScore(100);
    s2.SetScore(100);  // エラー
    p1->SetScore(100);
    p2->SetScore(100); // エラー

    std::cout << s1.GetScore() << "\n"
              << s2.GetScore() << "\n"
              << p1->GetScore() << "\n"
              << p2->GetScore() << std::endl;
}

SetScoreメンバ関数は constメンバ関数でないので、const修飾子付きの s2 および、 constポインタ経由でアクセスしている p2 からは呼び出すことができません。

constメンバ関数に限らず、const修飾子は、C++プログラミングにおいては非常に重要な機能であると言えます。 積極的に使うようにしていきましょう。

mutable

constメンバ関数は非常に重要かつ有用な機能ですが、稀に、思うように使えない場面があります。 例えば、結果をキャッシュしておくような場合が挙げられます。

class Accessor {
public:
    const char* GetData() const;

private:
    const char* mData;
};

const char* Accessor::GetData() const
{
    if (mData == nullptr) {
        mData = /* ~ */;  // エラー。constメンバ関数内ではメンバ変数を書き換えられない。
    }
    return mData;
}

Accessor::GetData() の実装内容の一部がコメントになっていますが、 例えば、ネットワークを通してデータを取得してくるような、非常に処理に時間が掛かることが書かれているとして下さい。 時間が掛かるため、はじめて Accessor::GetData() が呼び出されたときにだけ、その処理を行い、 次回以降の呼び出しでは、取得済みのデータを return するだけで済ませたいとします。

事情がどうであれ、意味的にはメンバ変数を返すだけなので、constメンバ関数にしたいところですが、 初回呼び出し時にだけは、メンバ変数への代入が必要であるため、constメンバ関数にはできなさそうです。

この問題を解決するには、普通に考えれば、次のようにメンバ関数を分離するしかありません。

class Accessor {
public:
    void CreateData();
    const char* GetData() const;
	
private:
    const char* mData;
};

void Accessor::CreateData()
{
    if (mData == nullptr) {
        mData = /* ~ */;
    }
}

const char* Accessor::GetData() const
{
    return mData;
}

この方法が良くないという訳ではありませんが、このクラスの利用者側としては手順や注意点が増えることになります。
そこで、constメンバ関数からでも、書き換え可能なメンバ変数を作る機能があります。 書き換え可能にしたいメンバ変数の宣言時に、mutableキーワードを付加しておきます。

class Accessor {
public:
    const char* GetData() const;

private:
    mutable const char* mData;  // constメンバ関数からでも書き換えてよい
};

const char* Accessor::GetData() const
{
    if (mData == nullptr) {
        mData = /* ~ */;  // OK
    }
    return mData;
}

mutable は、その存在意義を正しく理解していないと、誤った使い方をしてしまうでしょう。 そもそも、constメンバ関数は、メンバ変数を書き換えられないようにすることで、 オブジェクトの状態を変更しないようにする機能ですから、mutable は明らかにその本質に反しているようです。 これは、「オブジェクトの状態を変更しない」という部分をどのように捉えるかが重要です。

もし、「1ビットたりとも書き換えない」ということだと捉えるのであれば、 mutable は決して使ってはいけません
しかし、「クラスの外から変更の有無が分からなければ、こっそり書き換えられていても構わない」と考えるのなら、 mutable は(使い方を誤らなければ)問題の無い機能です

inline関数

ところで、「student.mScore = 100;」で済むところを「student.SetScore(100);」と書いたり、 「score = student.mScore;」で済むところを「score = student.GetScore();」と書いたりすることで、 関数呼び出しのコストが増えて、効率が落ちるのが嫌だという人もいるかも知れません。

そもそも、この程度のことで、目に見えてパフォーマンスが低下することはほとんどありませんが、 仮に問題となった場合には、メンバ関数をインライン関数にして、対応できます。

インライン関数とは、コンパイル時に、その関数の中身が呼び出し元のコードに展開(インライン展開)される関数です。つまり、あたかも、呼出し元のところに、関数の中身が書かれているかのように動作させることができます。

関数をインライン関数にするには、関数宣言時に先頭に inline を付加します。
ただし、本当にインライン展開が行われるかどうかは、コンパイラの裁量に委ねられています。インライン展開に向かない、あるいはできないと判断された場合には、inline の指定は無視され、通常の関数のように扱われます。

次のコードは、インライン関数の例です。

// Student.h(一部省略)

class Student {
public:
    inline void SetScore(int score);
    inline int GetScore() const;

private:
    int          mScore;  // 得点
};

// インライン関数の定義は、ヘッダファイル内に書く

void Student::SetScore(int score)
{
    assert(0 <= score && score <= 100);
    mScore = score;
}

int Student::GetScore() const
{
    return mScore;
}

他のソースファイルから呼び出す場合、展開するコードが見えなくてはならないため、 インライン関数の定義は、ヘッダファイル側に書きます。

また、次のように関数宣言と定義をまとめてしまうことも可能です。 この場合、inline を付けなくても構いません。

class Student {
public:
    void SetScore(int score)
    {
        assert(0 <= score && score <= 100);
        mScore = score;
    }
    int GetScore() const
    {
        return mScore;
    }

private:
    int          mScore;  // 得点
};

たまに面倒臭がって、インライン関数にするつもりもなく、メンバ関数の中身を上のように書いてしまう人がいますが、 この書き方は、インライン関数にするという意志表示になるので注意して下さい。

クラスと構造体の違い

クラスと構造体は、"ほぼ"同一のものです。 違いは、アクセス指定子のデフォルトが、クラスは「非公開」であるのに対して、構造体は「公開」であることだけです。 従って、次の CStudentクラスと、SStudent構造体は、意味としては同一になります。

class CStudent {
public:
    void SetScore(int score);

private:
    int mScore;
};

struct SStudent {
    void SetScore(int score);  // デフォルトで public なので、これは「公開」

private:
    int mScore;
};

ただし、あくまでも別の型ですから、CStudent型の変数に SStudent型の値を代入するようなことはできません。

あえてクラスと構造体を使い分けるとすれば、C言語的な構造体が必要な場合には struct を使い、 そうでなければ class を使うということです。 つまり、「複数の変数の集合体」というだけなら、struct にしておくということです。
前に、クラスのメンバ変数は必ず「非公開」にすると書きましたが、 C言語的な構造体の意味合いで struct を使うのなら、例外的にメンバ変数を「公開」にしても良いでしょう。


練習問題

問題① setter と getter を両方用意する代わりに、メンバ変数のメモリアドレスを返すメンバ関数を用意することは、 良いアイディアでしょうか?

問題② 「非公開」なメンバ関数は、どのような用途に使えるでしょうか?

問題③ 次の「正方形」を表す構造体について、カプセル化の観点から、問題点を指摘して下さい。

// 正方形を表す構造体
struct Square {
    int    mSide;    // 1辺の長さ
    int    mArea;    // 面積
};


解答ページはこちら

参考リンク



更新履歴

'2017/7/14 新規作成。



前の章へ(第5章 クラス)

次の章へ(第7章 コンストラクタとデストラクタ)

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

Programming Place Plus のトップページへ


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