アクセス指定子 | Programming Place Plus C++編【言語解説】 第12章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


アクセス指定子

クラスに関係する重要な機能の1つに、アクセス指定子があります。これは、前章の Studentクラスの例の中ですでに登場しています。「public」がそれです。

アクセス指定子には public 以外に、privateprotected があります。この章では、public と private について取り上げます。protected については、第27章で解説します。

アクセス指定子は、メンバがどこからアクセスされることを許すのかを指示するものです。

アクセス指定子は、クラスか構造体の定義の中で、「public:」のようにラベルとして使うことで効果が発生します。何度も繰り返し使うことも、異なるアクセス指定子を混在させて使うことも自由です。

アクセス指定子は、その記述位置よりも後ろにあるメンバそれぞれに影響します。効果の終わりは、ほかのアクセス指定子が登場したときか、クラスや構造体の定義の終わりに達したときです。

public

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

#include <iostream>
#include <string>

class Student {

    // 以下のメンバは「公開」していない

    std::string  mName;   // 名前
    int          mGrade;  // 学年
    int          mScore;  // 得点

public:
    // 以下のメンバは「公開」している

    void SetData(std::string name, int grade, int score);
    void Print();
};

void Student::SetData(std::string name, int grade, int score)
{
    mName = name;
    mGrade = grade;
    mScore = score;
}

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

int main()
{
    Student student;

    student.SetData("Saitou Hiroyuki", 2, 80);  // OK
    student.Print();                            // OK
    student.mScore = 100;                       // コンパイルエラー
}

このサンプルプログラムの場合、SetDataメンバ関数、Printメンバ関数は「公開」されています。そのため、「student.SetData()」や「student.Print()」のような呼び出しが可能です。

一方、mName、mGrade、mScore は「公開」していません。そのため、「student.mScore」のように使うことはエラーになります。

private

private アクセス指定子は、メンバを「非公開」にします。「非公開」なメンバは、そのクラス自身からしかアクセスできません。

#include <iostream>
#include <string>

class Student {
private:
    // 以下のメンバは「非公開」

    std::string  mName;   // 名前
    int          mGrade;  // 学年
    int          mScore;  // 得点

public:
    // 以下のメンバは「公開」している

    void SetData(std::string name, int grade, int score);
    void Print();
};

void Student::SetData(std::string name, int grade, int score)
{
    mName = name;
    mGrade = grade;
    mScore = score;
}

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

int main()
{
    Student student;

    student.SetData("Saitou Hiroyuki", 2, 80);  // OK
    student.Print();                            // OK
    student.mScore = 100;                       // コンパイルエラー
}

クラスの場合、アクセス指定子を記述しなかったときのデフォルトが「非公開」です。そのため、このサンプルプログラムは、先ほど「public」のところで見たサンプルプログラムとまったく同じです。

クラスではなく構造体を使う場合は、アクセス指定子を記述しなかったときのデフォルトは「公開」です。C++ においては、クラスと構造体の違いはこれだけです。

クラスと構造体を使い分ける方針は自由に決めて構いませんが、C言語的な構造体が必要な場合には struct を使い、そうでなければ class を使うのが一般的でしょう。つまり、メンバ関数やアクセス指定子のような、C言語の構造体にない機能は必要なく、単に「複数の変数の集合体」が欲しいという場合には、struct を使うということです。

なお、好みの問題ですが、「公開」のメンバをクラス定義の先頭付近に集めて、「非公開」のメンバを後ろに集めることが多いです。これは、クラスを利用する立場で見れば、「非公開」のメンバには興味がないからです(呼び出せないので)。以降はこの考え方にしたがって、次のような順番で書くことにします。

class Student {
public:
    void SetData(std::string name, int grade, int score);
    void Print();

private:
    std::string  mName;   // 名前
    int          mGrade;  // 学年
    int          mScore;  // 得点
};


カプセル化

得点として正常な値の範囲が 0~100 だとしても、メンバ変数 mScore が「公開」されていたら、student.mScore = 500; のように書けてしまいます。これは、変数を直接書き換え可能であることによる典型的な問題点です。

もし、オブジェクトを経由してメンバ変数 mScore を書き換える手段が、SetDataメンバ関数経由だけに限定されていれば、次のように assert を仕込むことができます。

void Student::SetData(std::string name, int grade, int score);
{
    assert(0 <= score && score <= 100);

    mName = name;
    mGrade = grade;
    mScore = score;
}

このように、変数へのアクセス経路が限られていることによって、「あらかじめチェックを入れておける」ことは大きな利点です。間違った使い方をしていることを、早い段階で気付かせる効果があります。

「後からチェックを追加できる」という点も重要です。メンバ関数を経由しない student.mScore = newScore; のようなコードが、プログラム内に記述されてしまった後では、その1つ1つを探してチェックを入れて回ることは難しいでしょう。また、メンバ関数の実装内容を変更しても、使う側はそれを知る必要がありません(引数も戻り値も名前も変わらないのなら、引き続き同じコードがコンパイルできるはず)。

大原則として、クラスのメンバ変数は必ず「非公開」にしてください。そして、オブジェクトから可能な操作を「公開」されたメンバ関数として定義します。こうすることで、カプセル化を促進することにつながります。

カプセル化は OOP の用語です。カプセル化とは、データや処理などの具体的な部分を、使う側から見えないように隠して、抽象化を図ることをいいます。

残念ながら C++ の文法上、完全に見えないようにすることは不可能です。しかし、アクセス指定子によって、使えないようにはできます。プログラマーの意識としては、使えない部分は見ないようにする(考えないようにする)ことが重要です。

実装の具体的な部分のことを、実装の詳細といいますが、使う側の立場からは、実装の詳細に依存してはいけません。「ソースコードを見るかぎり、こう実装されているようだから、こう使えばいい」という把握のしかたは間違っています。そうではなく、「公開」されているメンバの宣言と、そこに付随しているコメントやドキュメントだけを見て判断します。

setter と getter

メンバ変数は原則として「非公開」とすべきだという話でした。では、クラスを使う側が、そのメンバ変数の値を知りたいと思ったらどうするべきでしょうか。

そもそも、カプセル化の考え方からすると、クラスを使う側からはメンバ変数は見えていないのが正解です。その「非公開」なメンバ変数の値を知りたいと思うこと自体が不適切です。

たとえば、得点を記憶する mScoreメンバ変数の存在を知っていて、その値を欲しいと思うのではなくて、「生徒の得点を知りたい」というふうに思うことは適切です。生徒の得点が、mScore に入っていると知っていること自体、不用意に実装の詳細に立ち入ってしまっています。

一方、クラスを作る側としては、使う側が「生徒の得点を知りたい」と考えることが、もっともな要望なのかどうかを検討しましょう。適切であると思うのなら、GetScore のようなメンバ関数を作って「公開」します。不適切であると思うのなら、そのような手段を用意してはなりません。

ところで、このような考え方に反して、メンバ変数の値を変更したり、取得したりするシンプルなメンバ関数を「公開」する実装はよく見かけます。

#include <cassert>

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) と呼ばれます。SetScoreメンバ関数がセッターです。

セッターの名前には、「Set~」が使われることが多いです。

メンバ変数から値を取得することだけを目的とするメンバ関数は、ゲッター(getter) と呼ばれます。GetScoreメンバ関数がゲッターです。

ゲッターの名前には、「Get~」あるいは、変数名に合わせた「Score」のような名前が使われることが多いです。

メンバ変数を宣言したらつねに、セッターやゲッターを用意せよということではありません。結果的にセッターやゲッターを用意することが適切な場合はありますが、最初に述べた考え方を誤らないようにしてください。特にセッターが必要になるケースは少ないはずです。

constメンバ関数

ゲッターについては、その実装方法に注意が必要です。

たとえば、次のプログラムはコンパイルエラーになります。

#include <cassert>
#include <iostream>

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;
}

int main()
{
    Student student;
    student.SetScore(80);

    const Student* p = &student;
    std::cout << p->GetScore() << std::endl;  // コンパイルエラー
}

このプログラムがコンパイルエラーになる原因は、GetScoreメンバ関数を constポインタを経由して呼び出しているからです。constポインタが指し示す先の変数の値は変更できませんが、通常のメンバ関数呼び出しも、値(メンバ変数の値)を変更する可能性がある行為であるとみなされるため、禁止されています。これは、ポインタ経由でなくても同様です。

const Student student;
std::cout << p->GetScore() << std::endl;  // コンパイルエラー

しかし、ゲッターの意味合いや、実際の実装内容を考えてみても、メンバ変数の値を書き換えないので、エラーにされることには問題があります。むしろ const であっても、値の「取得」は行えるべきです。

そこで、「このメンバ関数はメンバ変数の書き換えを行わない」ということを、明示的に示します。

// 宣言
戻り値の型 メンバ関数名(仮引数の並び) const;

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

このように、const を付加されたメンバ関数を、constメンバ関数と呼びます。

constメンバ関数内では、メンバ変数の値を書き換えることができません。こうすることで、const付きのオブジェクトや、constポインタを経由しても呼び出し可能なメンバ関数になります。

#include <cassert>
#include <iostream>

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

private:
    int          mScore;  // 得点
};

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

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

int main()
{
    Student student;
    student.SetScore(80);

    const Student* p = &student;
    std::cout << p->GetScore() << std::endl;  // OK
}

実行結果:

80


インラインなメンバ関数

メンバ変数を直接アクセスさせずに、メンバ関数を経由させるようにすると、関数呼び出しのコストが加わることが気になる人もいるかもしれません。

その程度のコストが問題になるようなプログラムはほとんどないはずですが、必要であれば、インライン関数(第10章)にすることで効率を維持できるかもしれません。

// student.h

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

#include <cassert>

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

private:
    int          mScore;  // 得点
};

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

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

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

#endif
// main.cpp

#include <iostream>
#include "student.h"

int main()
{
    Student student;

    student.SetScore(80);
    std::cout << student.GetScore() << std::endl;
}

実行結果:

80

第10章で触れたとおり、インライン関数を呼び出す側のソースファイルに、インライン関数の定義がなければならないので、定義はヘッダファイルに記述します。

あるいは、クラス定義の中に、メンバ関数の定義を書く方法もあります。この場合、inline指定子を付加せずとも、インライン関数であるものとみなされます。

// student.h

#include <cassert>

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

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

private:
    int          mScore;  // 得点
};

#endif
// main.cpp

#include <iostream>
#include "student.h"

int main()
{
    Student student;

    student.SetScore(80);
    std::cout << student.GetScore() << std::endl;
}

実行結果:

80

この方が記述量が少ないし、一か所で書けて楽なので、インライン関数にするつもりでなく、この書き方を使う人がいますが、これはインライン関数にするという意志表示になるので注意してください。

インライン関数の使用には慎重になるべきです。第10章で述べたように、そもそもインライン展開が行われるかどうかはコンパイラの判断に委ねられますし、inline を付けずとも最適化される可能性もあります。実行効率の改善の必要性がはっきりしてから、インライン関数を使う検討を行い、使う場合も本当に効果が出ているのかどうかをよく調べるようにしてください。


練習問題

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

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

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

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


解答ページはこちら

参考リンク


更新履歴

’2018/8/30 全体的に見直し修正。
章のタイトルを変更(「カプセル化」–>「アクセス指定子」)

’2018/7/19 「setter と getter」の項のインライン関数に関する解説を整理。

’2014/4/24 新規作成。



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

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

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

Programming Place Plus のトップページへ



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