この章の概要です。
クラスに関係する重要な機能の1つに、アクセス指定子があります。これは、前章の Studentクラスの例の中ですでに登場しています。「public」がそれです。
アクセス指定子には public 以外に、private と protected があります。この章では、public と private について取り上げます。protected については、第35章で解説します。
アクセス指定子は、メンバがどこからアクセスされることを許すのかを指示するものです。
アクセス指定子は、クラスか構造体の定義の中で、「public:」のようにラベルとして使うことで効果が発生します。何度も繰り返し使うことも、異なるアクセス指定子を混在させて使うことも自由です。
アクセス指定子は、その記述位置よりも後ろにあるメンバそれぞれに影響します。効果の終わりは、ほかのアクセス指定子が登場したときか、クラスや構造体の定義の終わりに達したときです。
public アクセス指定子は、メンバを「公開」します。「公開」というのは、そのクラスのオブジェクトを使って、「student.Print();」とか「student->Print();」のようにしてアクセスできるということです。クラス定義の外部から使える(見える)ということで「公開」と表現されます。
#include <cstring>
#include <iostream>
class Student {
// 以下のメンバは「公開」していない
char mName[128]; // 名前
int mGrade; // 学年
int mScore; // 得点
public:
// 以下のメンバは「公開」している
void SetData(const char* name, int grade, int score);
void Print();
};
void Student::SetData(const char* name, int grade, int score)
{
std::strcpy(mName, name);
= grade;
mGrade = score;
mScore }
void Student::Print()
{
std::cout << "mName: " << mName << "\n"
<< "mGrade: " << mGrade << "\n"
<< "mScore: " << mScore << std::endl;
}
int main()
{
;
Student student
.SetData("Saitou Hiroyuki", 2, 80); // OK
student.Print(); // OK
student.mScore = 100; // コンパイルエラー
student}
このサンプルプログラムの場合、SetDataメンバ関数、Printメンバ関数は「公開」されています。そのため、「student.SetData()」や「student.Print()」のような呼び出しが可能です。
一方、mName、mGrade、mScore は「公開」していません。そのため、「student.mScore」のように使うことはエラーとなります。
private アクセス指定子は、メンバを「非公開」にします。「非公開」なメンバは、そのクラス自身からしかアクセスできません。
#include <cstring>
#include <iostream>
class Student {
private:
// 以下のメンバは「非公開」
char mName[128]; // 名前
int mGrade; // 学年
int mScore; // 得点
public:
// 以下のメンバは「公開」している
void SetData(const char* name, int grade, int score);
void Print();
};
void Student::SetData(const char* name, int grade, int score)
{
std::strcpy(mName, name);
= grade;
mGrade = score;
mScore }
void Student::Print()
{
std::cout << "mName: " << mName << "\n"
<< "mGrade: " << mGrade << "\n"
<< "mScore: " << mScore << std::endl;
}
int main()
{
;
Student student
.SetData("Saitou Hiroyuki", 2, 80); // OK
student.Print(); // OK
student.mScore = 100; // コンパイルエラー
student}
クラスの場合、アクセス指定子を記述しなかったときのデフォルトが「非公開」です。そのため、このサンプルプログラムは、先ほど「public」のところで見たサンプルプログラムとまったく同じです。
クラスではなく構造体を使う場合は、アクセス指定子を記述しなかったときのデフォルトは「公開」です。C++ においては、クラスと構造体の違いはこれだけです。
クラスと構造体を使い分ける方針は自由に決めて構いませんが、C言語的な構造体が必要な場合には struct を使い、そうでなければ class を使うのが一般的でしょう。つまり、メンバ関数やアクセス指定子のような、C言語の構造体にはない機能は必要なく、単に「複数の変数の集合体」が欲しいという場合には、struct を使うということです。
なお、好みの問題ですが、「公開」のメンバをクラス定義の先頭付近に集めて、「非公開」のメンバを後ろに集めることが多いです。これは、クラスを利用する立場で見れば、「非公開」のメンバには興味がないからです(呼び出せないので)。今後はこの考え方にしたがって、次のような順番で書くことにします。
class Student {
public:
void SetData(const char* name, int grade, int score);
void Print();
private:
char mName[128]; // 名前
int mGrade; // 学年
int mScore; // 得点
};
得点として正常な値の範囲が 0~100 だとしても、メンバ変数 mScore
が「公開」されていたら、student.mScore = 500;
のように書けてしまいます。これは、変数を直接書き換え可能であることによる典型的な問題点です。
もし、オブジェクトを経由してメンバ変数 mScore を書き換える手段が、SetDataメンバ関数経由だけに限定されていれば、次のように assert を仕込むことができます。
void Student::SetData(const char* name, int grade, int score);
{
assert(0 <= score && score <= 100);
std::strcpy(mName, name);
= grade;
mGrade = score;
mScore }
このように、変数へのアクセス経路が限られていることによって、「あらかじめチェックを入れておける」ことは大きな利点です。間違った使い方をしていることを、早い段階で気付かせる効果があります。
「後からチェックを追加できる」という点も重要です。メンバ関数を経由しない
student.mScore = newScore;
のようなコードが、プログラム内に記述されてしまった後では、その1つ1つを探してチェックを入れて回ることは難しいでしょう。また、メンバ関数の実装内容を変更しても、使う側はそれを知る必要がありません(引数も戻り値も名前も変わらないのなら、引き続き同じコードがコンパイルできるはず)。
大原則として、クラスのメンバ変数は必ず「非公開」にしてください。そして、オブジェクトから可能な操作を「公開」されたメンバ関数として定義します。こうすることで、カプセル化を促進することにつながります。
カプセル化は OOP の用語です。カプセル化とは、データや処理などの具体的な部分を、使う側から見えないように隠して、抽象化を図ることをいいます。
残念ながら C++ の文法上、完全に見えないようにすることは不可能です。しかし、アクセス指定子によって、使えないようにできます。プログラマーの意識としては、使えない部分は見ないようにする(考えないようにする)ことが重要です。
実装の具体的な部分のことを、実装の詳細といいますが、使う側の立場からは、実装の詳細に依存してはいけません。「ソースコードを見るかぎり、こう実装されているようだから、こう使えばいい」という把握のしかたは間違っています。そうではなく、「公開」されているメンバの宣言と、そこに付随しているコメントやドキュメントだけを見て判断します。
メンバ変数は原則として「非公開」とすべきだという話でした。では、クラスを使う側が、そのメンバ変数の値を知りたいと思ったらどうするべきでしょうか。
そもそも、カプセル化の考え方からすると、クラスを使う側からはメンバ変数は見えていないのが正解です。その「非公開」なメンバ変数の値を知りたいと思うこと自体が不適切です。
たとえば、得点を記憶する 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);
= score;
mScore }
int Student::GetScore()
{
return mScore;
}
メンバ変数に値を設定することだけを目的とするメンバ関数は、セッター(setter) と呼ばれます。SetScoreメンバ関数がセッターです。
セッターの名前には、「Set~」が使われることが多いです。
メンバ変数から値を取得することだけを目的とするメンバ関数は、ゲッター(getter) と呼ばれます。GetScoreメンバ関数がゲッターです。
ゲッターの名前には、「Get~」あるいは、変数名に合わせた「Score」のような名前が使われることが多いです。
メンバ変数を宣言したらつねに、セッターやゲッターを用意せよということではありません。結果的にセッターやゲッターを用意することが適切な場合はありますが、最初に述べた考え方を誤らないようにしてください。特にセッターが必要になるケースは少ないはずです。
ゲッターについては、その実装方法に注意が必要です。
たとえば、次のプログラムはコンパイルエラーになります。
#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);
= score;
mScore }
int Student::GetScore()
{
return mScore;
}
int main()
{
;
Student student.SetScore(80);
student
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);
= score;
mScore }
int Student::GetScore() const
{
return mScore;
}
int main()
{
;
Student student.SetScore(80);
student
const Student* p = &student;
std::cout << p->GetScore() << std::endl; // OK
}
実行結果:
80
constメンバ関数は非常に重要かつ有用な機能ですが、まれに、思うように使えない場面があります。
たとえば、結果をキャッシュしておくように実装する場合が挙げられます。
#include <cstdlib>
class Accessor {
public:
();
Accessor~Accessor();
const void* Get() const;
private:
void* mData;
};
::Accessor() : mData(nullptr)
Accessor{
}
::~Accessor()
Accessor{
std::free(mData);
}
const void* Accessor::Get() const
{
if (mData == nullptr) {
= std::malloc(1024 * 1024); // constメンバ関数内ではメンバ変数を書き換えられない。
mData }
return mData;
}
return するデータを作り出すこと自体が、非常にコストが掛かるものであり(たとえば、ネットワークを通してデータを取得してこなければならないだとか、単に非常に巨大であるだとか)、また、条件分岐の仕方によっては、そのデータが使われないこともあるのであれば、初めて Accessor::Get関数が呼び出されたときにだけ、データを作ることが望ましいでしょう。
そのような要件で実装する場合、Accessor::Get関数の中でデータを作りたいところですが、そうすると、メンバ変数 mData を書き換える必要が出てくるため、constメンバ関数にできなくなってしまいます。
Accessor::Get関数は、返すべきデータがすでにあれば、単にそのデータのメモリアドレスを return するだけです。この章の冒頭で書いたとおり、const が使えるのなら積極的に使うべきですし、ゲッター系のメンバ関数は、constオブジェクトからでも呼び出せた方がいいでしょうから、constメンバ関数にしたいところです。
この問題を解決するには、普通に考えれば、次のようにメンバ関数を分離するしかありません。
#include <cstdlib>
class Accessor {
public:
();
Accessor~Accessor();
void CreateData();
const void* Get() const;
private:
void* mData;
};
::Accessor() : mData(nullptr)
Accessor{
}
::~Accessor()
Accessor{
std::free(mData);
}
void Accessor::CreateData()
{
if (mData == nullptr) {
= std::malloc(1024 * 1024);
mData }
}
const void* Accessor::Get() const
{
return mData;
}
こうするとまず、オブジェクトが const であるのなら Accessor::CreateData関数を呼び出せない問題があります。また、Accessor::CreateData関数を呼び忘れないようにしなければならないので、このクラスの使い方がシンプルでなくなってしまいます。
そこで、mutableキーワードを使う方法があります。mutable は、メンバ変数の宣言時に付加します。
class X {
mutable 型名 メンバ変数名;
};
mutable が付加されたメンバ変数は、constオブジェクトであっても書き換え可能になります。そのため、constメンバ関数内からでも書き換えることができます。
struct X {
int v1;
mutable int v2;
};
int main()
{
const X cx;
.v1 = 100; // コンパイルエラー
cx.v2 = 100; // OK
cx}
mutable を使って Accessorクラスを直すと、次のようになります。
#include <cstdlib>
#include <iostream>
class Accessor {
public:
();
Accessor~Accessor();
const void* Get() const;
private:
mutable void* mData; // constメンバ関数からでも書き換えられる
};
::Accessor() : mData(nullptr)
Accessor{
}
::~Accessor()
Accessor{
std::free(mData);
}
const void* Accessor::Get() const
{
if (mData == nullptr) {
= std::malloc(1024 * 1024); // OK
mData }
return mData;
}
int main()
{
;
Accessor accessor
std::cout << accessor.Get() << std::endl;
std::cout << accessor.Get() << std::endl;
}
実行結果:
014D9040
014D9040
mutable は、その存在意義を正しく理解しないと、誤った捉え方・使い方をしてしまうでしょう。mutable は、せっかくの const による安全対策に穴を空けているように思えます。
const の意味を、オブジェクトを「1ビットたりとも書き換えない」のだと捉えるのであれば、mutable は確かにおかしな機能です。しかし、「クラスの外から変更の有無が分からなければ、書き換えても構わない」と捉えるのなら、mutable は(使い方を誤らなければ)問題のない機能です。
このような定数性の考え方を、論理的定数性と呼ぶことがあります。
たとえば、関数を呼び出した回数をカウントしておくだとか、計算結果をキャッシュしておいて、次回以降は計算をスキップして結果だけ返すようにするといった使い方は適切です。
このようなケースで mutable を使わないことにこだわるあまり、const を使うこと自体を諦めてしまうとか、const_cast を使うといった手段に走るのなら、むしろ悪い方向に進んでいるといえるでしょう。
メンバ変数を直接アクセスさせずに、メンバ関数を経由させるようにすると、関数呼び出しのコストが加わることが気になる人もいるかもしれません。
その程度のコストが問題になるようなプログラムはほとんどないはずですが、必要であれば、インライン関数にすることで効率を維持できるかもしれません。
インライン関数とは、宣言時に inline指定子を付加された関数のことです。
inline void f();
もちろん、関数の本体も記述して、定義にしても構いません。
inline void f() {}
インライン関数は、これを呼び出そうとするすべてのソースファイルに、それぞれまったく同一内容の定義がなければなりません。そのため、複数のソースファイルから呼び出したいのであれば、インライン関数の定義をヘッダファイルに記述して、各ソースファイルからインクルードさせるようにします。
inline指定子は、インライン展開を行うことを要請します。インライン展開とは、関数の本体のコードを、その関数を呼び出している箇所に展開することです。これは、関数形式マクロがしていることと実質的に同じといえます。
【上級】インライン展開は多くの場合、コンパイル時に行われますが、リンク時など、ほかのタイミングで行われる可能性もあります。
インライン展開が行われることで、関数呼び出しのコストを避けられます。一方で、呼び出し箇所ごとにコードが重複することになるので、プログラムのサイズは大きくなるかもしれません。
インライン関数の本体が小さければ、かえってプログラムサイズが小さくなる可能性はあります。
inline指定子は、インライン展開して欲しいという要請に過ぎず、強制的な指示ではありません。インライン展開することが難しい場面もあるため、要請は無視される可能性があります。インライン展開が行われなかった場合は、通常の関数呼び出しの仕組みが使われます。
【上級】ループを含む関数や、再帰呼び出しを行う関数、仮想関数(第35章)では、inline指定子の要請を無視することがあります。
【上級】ある呼び出し箇所ではインライン展開され、ほかの呼び出し箇所ではインライン展開されないということも起こり得ます。たとえば、インライン関数のメモリアドレスを取得している箇所があると、関数の実体が必要になるため、インライン展開を行わないはずです。しかし、それとは別の箇所でインライン展開可能なら、効率を優先して、インライン展開を行うかもしれません。
反対に、inline指定子を付加していなくとも、通常の最適化の一環として、関数がインライン展開されることもあります。
結局、inline指定子を付加してもしなくても、インライン展開は行われるかもしれないし、行われないかもしれないということです。インライン関数を使うことによる多少の弊害もあるため、inline指定子を付けることには慎重であるべきです。
弊害とは、たとえば、インライン関数の本体のコードをヘッダファイル側に露出しなければならないため、後から実装を変更すると、呼び出し側の再コンパイルを必要とすることなどがあります。大規模な開発では、プログラムのビルド時間も開発効率に大きく影響します。
次のコードは、インライン関数の例です。
// 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);
= score;
mScore }
inline int Student::GetScore() const
{
return mScore;
}
#endif
// main.cpp
#include <iostream>
#include "student.h"
int main()
{
;
Student student
.SetScore(80);
studentstd::cout << student.GetScore() << std::endl;
}
実行結果:
80
インライン関数を呼び出す側のソースファイルに、インライン関数の定義がなければならないので、定義はヘッダファイルに記述します。
あるいは、クラス定義の中に、メンバ関数の定義を書く方法もあります。この場合、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);
= score;
mScore }
int GetScore() const
{
return mScore;
}
private:
int mScore; // 得点
};
#endif
// main.cpp
#include <iostream>
#include "student.h"
int main()
{
;
Student student
.SetScore(80);
studentstd::cout << student.GetScore() << std::endl;
}
実行結果:
80
この方が記述量が少ないし、一か所で書けて楽なので、インライン関数にするつもりでなく、この書き方を使う人がいますが、これはインライン関数にするという意志表示になるので注意してください。
問題① setter と getter を両方用意する代わりに、メンバ変数のメモリアドレスを返すメンバ関数を用意することは、良いアイディアでしょうか?
問題② 「非公開」なメンバ関数は、どのような用途に使えるでしょうか?
問題③ 次の「正方形」を表す構造体について、カプセル化の観点から、問題点を指摘してください。
// 正方形を表す構造体
struct Square {
int mSide; // 1辺の長さ
int mArea; // 面積
};
C++編【言語解説】第15章「const の活用」の修正に合わせて、「mutable」の項の内容を更新。
C++編【言語解説】第12章「アクセス指定子」の修正に合わせて、内容更新。
章のタイトルを変更(「カプセル化」–>「アクセス指定子」)
「inline関数」の項の内容を、C++編での更新に合わせて、全面的に書き換えた。
新規作成。
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
RSS | 管理者情報 | プライバシーポリシー |