C++ カプセル化【アクセス指定子を使ってデータを外部から守れ】

C++
この記事は約13分で読めます。

こんにちは、ナナです。

「カプセル化」とは、オブジェクト指向3大機能の1つであり、オブジェクトに対する最も基本的な考え方を知ることができます。

カプセル化は比較的わかりやすい機能のため、まずはこの「カプセル化」を攻略していきましょう。

本記事で学習できること
  • 「カプセル化」の概念と必要性とは何なのか?
  • アクセス指定子の役割と種類とは?
  • アクセス指定子を使ったカプセル化の保護機能とは?
  • カプセル化したオブジェクトへのアクセス方法とは?
スポンサー

カプセル化:外部からオブジェクトのデータを守れ!

押忍!プログラミングを学んでるのに「カプセル」ってどういうことっすか?

自分はこの肉体を作るために、カプセル型のサプリは欠かせないっすよ。プログラムも栄養補強が必要なんすか?

ナナ
ナナ

「カプセル化」っていうのは、薬やガチャガチャのおもちゃで利用するカプセルをイメージした言葉です。これらのものがどうしてカプセルに包まれているのでしょう?

その理由がわかれば「カプセル化」とは何かが理解できそうですね。

外部からメンバ変数にアクセスされることの危険性

クラスオブジェクトとは「メンバ変数」と「メンバ関数」により構成され、メンバ変数にはオブジェクトが管理する重要なデータが保管されています。

つまり、オブジェクトにとって「メンバ変数」とは命に等しいほど大事なものなのです。

メンバ変数は命

それでは、次のプログラムでメンバ変数にどのようにアクセスしているかを考察しましょう。

#include <stdio.h>

class POS
{
public:
    int x;
    int y;
};

int main()
{
    POS pos;

    pos.x = 100;
    pos.y = 200;

    printf("x:%d y:%d\n", pos.x, pos.y);

    return 0;
}

POSクラスという型定義側と、オブジェクトを生成し利用する側の2つの視点で見てみましょう。白抜きの部分は、オブジェクトを生成し利用するmain側のプログラムと言えます。

ここでオブジェクト内のメンバ変数「x」「y」の値を書き換えています。

外部からアクセスされるのは危険

例えば「X」「Y」の座標が最大100までの範囲でしか動作しない場合、この「pos.y = 200」は危険な書き換えと言えます。

つまり、この処理はオブジェクト外部であるmain側が、オブジェクトの命である「メンバ変数」に直接手を出す危険な行為と言えます。

このような危険な行為をオブジェクトは防がなくてはなりません。そのために「カプセル化」という機能を利用するのです。

ナナ
ナナ

なぜ「カプセル化」という機能が用意されたのかを知るのです。その理由を知ることで本当の意味で「カプセル化」という機能が理解できるのです。

「カプセル化」とはデータを保護する膜を用意すること

薬やガチャガチャでよく利用される「カプセル」というものは何のために使われるのでしょう?

それは、周りからの刺激に対して対象物を保護する役割があるのです。

カプセルの役割

オブジェクト指向の「カプセル化」とは、まさしくこのように外部のデータアクセスからメンバ変数を保護するための仕組みです。

ナナ
ナナ

オブジェクトのデータをどのように危険なプログラムから保護するかという方法を模索した結果、「カプセル化」という考え方が生まれたのです。

スポンサー

クラスに搭載された「アクセス指定子」というカプセル保護機能

押っ忍!なるほど、オブジェクトはちゃんと守ってやらないといけないっすね。自分は仲間は体を張って守るっすよ。

でも、オブジェクトはどうやって守ったらいいのかわからないっす。

ナナ
ナナ

オブジェクトをカプセルで保護するための機能は「クラス」に搭載されているんです。それを「アクセス指定子」と呼びます。

アクセス指定子の使い方を学んでいきましょう。

アクセス指定子の役割と種類

クラスには「アクセス指定子していし」と呼ばれる機能があります。この機能こそがオブジェクトに対するアクセスをコントロールするための役割を持っているのです。

アクセス指定子は3種類用意されています。

アクセス指定子公開範囲役割
public完全公開オブジェクト外部からのアクセスを許可する。
protected一部公開オブジェクト外部からのアクセスを禁止する。
ただし、継承先クラスからは許可する。
private非公開オブジェクト外部からのアクセスを禁止する。
継承先クラスも禁止する。

今の段階で重要なのは「public」と「private」の違いです。

class POS
{
public:
    int x;
    int y;
};

これまでに利用してきた上記クラスでは「x」「y」が「public」なアクセス指定となっているため、オブジェクト外部からの参照が可能だったのです。

つまり、「public」を「private」にすることでオブジェクト外部からのアクセスを禁止させることができるようになります。

ナナ
ナナ

「protected」に関してはクラスの「継承」機能を紹介するときに合わせて解説しましょう。今は忘れてください。

「private」アクセス指定子を使ったカプセル化

それでは実際に「private」に変更することで、どうなるのかを実験してみましょう。

#include <stdio.h>

class POS
{
private:
    int x;
    int y;
};

int main()
{
    POS pos;

    pos.x = 100;
    pos.y = 200;

    printf("x:%d y:%d\n", pos.x, pos.y);

    return 0;
}

このプログラムをビルドすると、次のようにビルドエラーが発生します。

main.cpp(14,8): error C2248: 'POS::x': private メンバー (クラス 'POS' で宣言されている) にアクセスできません。
main.cpp(15,8): error C2248: 'POS::y': private メンバー (クラス 'POS' で宣言されている) にアクセスできません。

このようにオブジェクト外部であるmain関数からのアクセスを遮断することができました。

ナナ
ナナ

これがC++における「カプセル化」の方法です。メンバ変数は基本的に「private」で保護するのが一般的な形として覚えておきましょう!

スポンサー

カプセル化されたオブジェクトのメンバ変数にアクセスする方法

押忍っ!これで自分もオブジェクトを守ることができるようになったっす。「private」で守るっすね。

あれ?ちょっと待ってくださいっす。メンバ変数を「private」で守ったら、どうやってメンバ変数の値を書き換えることができるっすか?

ナナ
ナナ

よく気づいたね、そうなんだよ。確かに「private」にすることで外部からメンバ変数へのアクセスはできなくなったよね。

でも、オブジェクト外部から「メンバ変数にアクセスしたい!」というニーズにどうやって答えるかが、今度は問題になるんだね。

「カプセル化」という機能は、何が何でも外部からアクセスさせないってことではないんです。

「カプセル化」が防いでいるのは、外部からの直接的なメンバ変数へのアクセスを禁じているのです。

では、直接ではないアクセスとは何か?そこに答えがあるのです。

メンバ関数を使った間接的なメンバ変数へのアクセス

メンバ関数とは、メンバ変数を制御するための関数です。

つまり、オブジェクト外部からメンバ変数の値を参照したり書き換えたりしたければ、「メンバ関数」という窓口を通せばよいのです。

メンバ関数はメンバ変数への窓口

それでは具体的なプログラムで示しましょう。XY座標にアクセスするための「setPos」メンバ関数を「public」アクセス指定子で定義しました。

#ifndef POS_H
#define POS_H

class POS
{
private:
    int x;
    int y;

public:
    POS();
    int setPos(int tmpx, int tmpy);
};

#endif  //  POS_H
#include "POS.h"

//  コンストラクタ
POS::POS()
{
    x = 0;
    y = 0;
}

//  メンバ設定するためのメンバ関数
int POS::setPos(int tmpx, int tmpy)
{
    x = tmpx;
    y = tmpy;

    return 0;
}

このようにアクセス指定子には、メンバ変数だけでなくメンバ関数にも指定することができます。

オブジェクトを利用する側は、次のようにpublicで公開された「setPos」メンバ関数を呼び出すことで任意の座標を設定することができます。

#include <stdio.h>
#include "POS.h"

int main()
{
    POS pos;

    //  座標設定
    pos.setPos(100, 200);

    return 0;
}

今回の「setPos」メンバ関数では、X座標/Y座標はそれぞれ単体で書き換えることができず、両方を一度に更新することしかできません。

メンバ関数を利用することで、このような設定方法を強制することもできるということです。

ナナ
ナナ

アクセス指定子は、メンバ変数にもメンバ関数にも適用することができます。setPos関数はオブジェクト外部から呼び出したいため「public」な領域に定義します。

アクセス指定子の基本は「メンバ変数はprivate」「メンバ関数はpublic」です。

メンバ関数を窓口とするメリット

これで「setPos」メンバ関数を利用して、XY座標の値を変えることはできました。

しかしですよ

わざわざ関数を経由することになって、面倒になっただけじゃないの?

という疑問がありますよね。

違うのです。メンバ関数を利用してアクセスすることにはメリットがあるのです。

それは、アクセスの正当性をメンバ関数でチェックすることができるということです。

例えば、X・Y座標の設定範囲が「-100~100」の間しか許されないという状況になった場合、メンバ関数であれば設定範囲を事前に確認することが可能となるのです。

int POS::setPos(int tmpx, int tmpy)
{
    //  範囲チェック
    if (tmpx < -100 || 100 < tmpx || tmpy < -100 || 100 < tmpy)
    {
        return -1;
    }

    x = tmpx;
    y = tmpy;

    return 0;
}

メンバ変数へダイレクトなアクセスを認めてしまうと、このような正当性のチェックができないのです。

ナナ
ナナ

メンバ関数はメンバ変数のボディーガードであり、不正なアクセスを防ぐことができるのです。

このようにメンバ関数を窓口にすることで、メンバ変数に関する外部からの制御を限定的にすることができるのです。

スポンサー

Q&A:カプセル化に関するよくある質問

Q:アクセス指定子の書き方にルールがあったら教えて

「public」や「private」を使ったアクセス指定子ってクラス定義の中で書き方のルールってあるんすか?

ナナ
ナナ

はい、ありますよ。いくつかのプログラム例を用いてルールを解説しましょう。

まずは、基本的なルールを示しておきましょう。

  1. アクセス指定子を1つも指定しなかった場合は「private」扱いとする
  2. アクセス指定子は次のアクセス指定子が登場するまで継続される
  3. アクセス指定子に記述順番はなく、同じアクセス指定子が複数登場してもよい

ルール1:アクセス指定子を記述しない場合、「private」指定とみなされる

#include <stdio.h>

class POS
{
    int x;
    int y;
};

int main()
{
    POS pos;

    pos.x = 100;  // ビルドエラーが発生

    return 0;
}

つまり、クラスにおけるデフォルトのアクセス指定子は「private」設定になっているということです。

そのため、メンバ変数「x」へ外部からアクセスしようとするとビルドエラーが発生します。

ルール2:アクセス指定子は次のアクセス指定子が登場するまで継続

class POS
{
private:
    int x;
    int y;      //  ここまで「private」が継続

public:
    int a;
    int b;
    int c;      //  ここまで「public」が継続
};

このようにアクセス指定子を記述した後は、連続してメンバ変数やメンバ関数を登録することで指定内容が継続されます。

次の書き方では、ルール1と組み合わせて「x」「y」メンバ変数は「private」の扱いとなります。

class POS
{
    int x;
    int y;      //  デフォルトの「private」が継続

public:
    int a;
    int b;
    int c;      //  ここまで「public」が継続
};

ルール3:アクセス指定子は順番に決まりはなく何度も記述できる

class POS
{
private:
    int x;
    int y;

public:
    int a;
    int b;
    int c;

private:
    int e;
    int f;

public:
    int g;
};

「private」から記述するといったルールはなく、自由に指定が可能です。そしてアクセス指定子は何度登場しても大丈夫です。

Q:構造体定義にアクセス指定子は使えないの?

クラスっていうのは「構造体」を拡張したものなんすよね。構造体には「アクセス指定子」は使えないんすか?

ナナ
ナナ

使うこと自体は可能です。ただし、一般的には使用しません。アクセス指定子で制御したい情報であれば「クラス」で定義すべきだからです。

次のように、構造体定義にアクセス指定子を使うこと自体は可能です。ただし、使えるというだけで一般的には使わないことを覚えておきましょう。

typedef struct
{
    int x;
    int y;          //  構造体ではデフォルトは「public」指定

private:
    int a;
    int b;
} POS;

int main()
{
    POS pos;

    pos.x = 100;    //  「x」は「public」のためアクセスできる

    return 0;
}

構造体におけるデフォルトのアクセス指定子は「public」な点が、クラスとは異なる部分です。

これは「private」を構造体のデフォルトアクセス指定子にしてしまうと、使う側が構造体メンバにアクセスできなくなってしまうからです。

C言語の互換性を保つためには「public」がデフォルトでないと成立しないのです。

Q:コンストラクタとデストラクタはどのアクセス指定子に定義するの?

押忍!「コンストラクタ」や「デストラクタ」もメンバ関数っすから、どこかのアクセス指定子に所属させる必要があるっすよね?「public」「private」のどっちがいいんすか?

ナナ
ナナ

よほど特殊な状況でない限り「public」にコンストラクタとデストラクタは配置します。

コンストラクタやデストラクタの定義は「public」なアクセス指定子に定義するのがセオリーです。

int main()
{
    POS pos;    //  コンストラクタが「public」でないと呼び出せない

    return 0;   //  デストラクタが「public」でないと呼び出せない
}

オブジェクトを生成・解体する側はクラス外部のプログラムであるため、「public」になっていないとコンストラクタとデストラクタを呼び出すことができないからです。

スポンサー
C++C++入門
スポンサー
モノづくりC言語塾