C/C++ - 言語仕様編 第8回 〜クラス - 前編 - 〜

カプセル化とクラス

プロパティ

オブジェクト c は、数値や文字のような具体的なものであったり、 あるいは、もっと抽象的な事象を扱う、ある対象として機能する存在である。

そのオブジェクトは、ある性質を持ち、それをもって対象を特徴づける。 もし、その性質が、例えば、T型で表現されるものであるとするならば、 C++において、オブジェクト c は、

class C{
	T n;
};

として定義される、T型メンバ変数 n を持つ クラス C に対して、

C* c=new C;

として構築される。

構造体と異なり、このオブジェクト *c は、c->nというメンバ変数を持ちながら、 それを、T型変数として認識することが出来ない。

OOP、オブジェクト指向プログラミングという考え方における、カプセル化という概念であり、 クラス内で定義される、外から見ることが出来ない変数を、プロパティ、 あるいは、構造体のときと同様に、区別することなく、メンバ変数と呼ぶ。

コンストラクタ

外から見ることが出来ないプロパティしかもたないクラスでは、 そのプロパティが持つ値を特徴とする、さまざまなインスタンスを構築することすらも出来ない。

したがって、ある部分で公開する必要があり、それを、public: で指示する。 public: と指示した以降は、オブジェクトを通して外部に対して可視化される。

クラスCのプロパティ n は、インスタンスの構築時にその値を定めることが出来る。

class C{
	T n;
public:
	C(const T& number);
};

C::C(const T& number):n(number){}

とすることで、インスタンスを構築する際に、T型オブジェクト t を用いて、

C* c=new C(t);

と初期値を指定することが出来る。これは、T型オブジェクトからC型オブジェクトへの コンストラクトキャストとして機能する事を意味し、また、このコンストラクタを用いてキャストが定義される。

また、T型オブジェクト t から、C(t)として、C型オブジェクトにキャストされることから、インスタンスを経由せずに、

C& c=C(t);

などとして、C&型オブジェクト c を構築することが出来る。

メソッド

プロパティは、インスタンスの特性を表現するものであり、オブジェクトは、このプロパティを 変化させることで、その特性を変えることが出来る。

クラスの宣言内部で、

class C{
...
public:
	const T& get(void);
	void set(const T& t);
};

などと関数のように宣言し、

const T& C::get(void){return n;}
void C::set(const T& t){n=t;}

などと定義すると、オブジェクト*cに対して、c->get()は、const T&型オブジェクトを返し、 T型オブジェクトtに対して、c->set(t); は、オブジェクトcが持つインスタンスのプロパティの値をtに変化させる。

このように、オブジェクトから呼び出され、インスタンスの持つプロパティに作用する関数を、 メソッド、あるいは、メンバ変数と同じニュアンスで、メンバ関数と呼ぶ。

const メソッド

const C型オブジェクトである場合、set()メソッドは値を書き換えてしまうため、実行するべきではない。 一方、get()メソッドは、const C型オブジェクトであっても、プロパティを書き換えるようなことはないため、 実行することは許可される。

get()メソッドの宣言を、

const T& get(void)const;

とし、その定義を、

const T& C::get(void)const{return n;}

とすることで、メソッドを呼び出すオブジェクトがconstであっても呼び出すことが出来る。 逆に、constメソッドでないものはconstなオブジェクトから呼び出すことが出来ない。

this ポインタ

クラスCには、それぞれ、自身を指すC*const型変数である、thisポインタが存在する。 従って、クラス内において自身のプロパティ n は、this->n として選択可能であり、また、オブジェクト自身は、*thisとして 取得する事が出来る。なお、this->n などとしなくとも、nは、クラス内で直接取得する事が可能である。

コンストラクタとデストラクタ

int型プロパティ lengthと、T*型プロパティ n を持つクラス Cを、次のように宣言し、定義する。

class C{
	int length;
	T* n;
};	

デフォルトコンストラクタ

引数をvoidとするコンストラクタ、

C(void);

は、デフォルトコンストラクタと呼ばれ、

C* c=new C;

などとした時の指示が行われる。配列インスタンスは、publicなデフォルトコンストラクタを持つクラスでのみ 構築可能である。逆に、基本型が配列インスタンスが構築できることからわかるように、 すべてのクラスは、明示的に書かなくともデフォルトコンストラクタを持つ。

public なデフォルトコンストラクタがクラス C に定義されているとき、

C c;

などとすると c は、デフォルトコンストラクタを用いて構築される、自動オブジェクトとなる。

T*型のプロパティを使う場合、デフォルトコンストラクタは、

C::C(void):length(0),n(0){}

のように、0で初期化するようにするのが望ましいとされる。

明示的コンストラクタ

length プロパティを指定するために、

C::C(const int l):length(l), n(new T[l]){}

などと定義した場合、当然、const intからCへのコンストラクトキャストが可能であるが、 このように1引数として定義すると、同時に、int型からC型への暗黙のキャストが定義される。 つまり、int型がC型オブジェクトとして機能するように動作させることが出来、 場合によっては非常に便利な機能となる。

しかし、これを、ただインスタンスの初期化のためのみに使用したいという場合は、宣言を、

explicit C(const int l);

とする。このとき、明示的にコンストラクタにconst intを指定することは可能であるが、コンストラクタを経由せずに、 intからCへの暗黙のキャストは禁止されるようになる。

初期化コンストラクタ

コンストラクタの引数は複数でもよく、

C::C(const int l, T*const t):length(l), n(t){}

としてもよい。このような場合、引数として常に2つが要求され、逆に、2つの引数が、 T*const型とconst int型にキャスト可能であれば、このコンストラクタが呼ばれる。

コピーコンストラクタ

引数を、const C&とするコンストラクタ、

C(const C& c);

は、コピーコンストラクタと呼ばれ、publicなコピーコンストラクタを持つクラスでのみ、 コピーが可能となる。逆に基本型が値コピー可能であることからわかるように、 すべてのクラスは、明示的に書かなくともコピーコンストラクタを持つ。

このように、暗黙のうちに定義されるコピーコンストラクタは、2つのプロパティをそのままコピーする。 それは、プロパティが値を表現するものであれば全く問題がないが、 T*型変数のようなポインタ変数をプロパティとして持つ場合、オブジェクトを値コピーすると、 コピー元とコピー先で共通のポインタをプロパティの値として持つことになる。

もし、この2つのインスタンスが、互いに独立したものであるならば、コピーコンストラクタは、

C::C(const C& c):length(c.length), n(new C[c.length]){
	for(int i=0;i<length;++i) n[i]=c.n[i];
}

などとして、直接値コピーするように書くべきである。

デストラクタ

このクラス C は、n が length の長さの配列インスタンスを持つポインタ変数であると考える場合、 このクラスを解体する際には、ともに、n のインスタンスも解体するべきであろう。 このように、オブジェクトを解体する際の指示は、

~C(void);

と宣言し、

C::~C(void){delete [] p;}

などと定義すればよい。

今回のクラスでは、初期化コンストラクタがあるため、完全にはdelete[]の安全性が保障できないものの、 この初期化コンストラクタが正しく使われるならば、それ以外に現時点で、内部のプロパティを不正に書き換える方法は 定義されていないため、インスタンスは安全に解体される。

カプセル化の意味は、隠蔽することというよりも、むしろ、自分で公開できるレベルをコントロールできるという ところに重きを置いている。完全に公開するのではなく、const参照の取得だけというような公開の度合いを 適宜指定するのが、正しい考え方である。

静的クラスと仮想クラス

静的プロパティ

プロパティを static をつけて宣言すると、静的プロパティとなる。 静的変数と意味は同様であるが、これを、ある特定のオブジェクトに依存せず、クラスに依存する変数であるという 言い換えをする事が出来る。

クラス C の静的プロパティ、

static T t;

は、静的プロパティコンストラクタ、

T C::t(t1);

によってt1に初期化される。

静的メソッドと静的クラス

クラスCのメソッドmの宣言時に、

static T m(A a, B b);

などと宣言すると、mは静的メソッドとなる。

通常、メソッドはオブジェクトから呼ばれ、インスタンスのプロパティに対して作用するものであるが、 静的メソッドはオブジェクトではなく、クラスに依存するメソッドである。

したがって、あるオブジェクトから呼ぶだけでなく、クラスから、

C::m(a1,b1);

と直接呼ぶ事が可能である。一方、特定のインスタンスに依存しないため、 静的メソッドからは、静的プロパティ以外のいかなるプロパティにもアクセスすることが出来ない。

すべてのプロパティとメソッドが静的であるクラスを、静的クラスという。

仮想メソッドと仮想クラス

クラスCのメソッドmの宣言時に、

virtual T m(A a, B b);

などと宣言すると、mは仮想メソッドとなる。

通常、メソッドはクラスを定義した時点でその振る舞いが決定されるが、 仮想メソッドは、インスタンスが生成され、それがあるオブジェクトとして操作対象となった瞬間に、 その振る舞いが動的に決定される。

また、メソッドだけでなく、デストラクタも仮想デストラクタとすることが出来、

virtual ~C(void);

などとすることで、デストラクタの処理が、オブジェクトとして成立した瞬間に決定される。

virtualをつけない非仮想メソッドは静的結合であるといい、virtualをつけた仮想メソッドを、 動的結合であるという。

動的結合を含むクラスを仮想クラスといい、動的結合を実現するための4バイトの記憶領域を 余分に必要とするようになる。

多態性とオーバーロード

ポリモーフィズム

オブジェクトがメソッドでインスタンスに対してある指示を行うとき、その指示が同じ意味を表すならば、 その呼び出すメソッドの名前は同じにすべきである。逆の言い方をすれば、メソッド名が同じであっても、 その指示の仕方によって、その挙動を変えることができる。 これが、OOPにおける、多態性、ポリモーフィズムと呼ばれる概念である。

メソッドのデフォルト引数

あるメソッドの宣言

m(A a, B b, C c, D d);

は、その通り、4つの引数を要求する。

もし、このメソッドを、3つの引数だけで呼ぶとした時に、D型オブジェクトとしてデフォルトでd1を用いるならば、

m(A a, B b, C c, D d=d1);

として宣言すればよい。引数の末端から順番にデフォルト引数を定めることが出来る。 従って、DとCのデフォルト引数を定めたり、あるいは、A〜Dまで全てのデフォルト引数を定めることは出来るが、 Aだけのデフォルト引数を定めたりすることは出来ない。

メソッドのオーバーロード

先ほど定義したメソッドm()に対して、さらに同じ名前の、

m(A a=a1, B b=b1);

などと定義した場合、例えば、引数なしの場合は、a1, b1がデフォルトとして入り、 1引数の場合は、b1のみがデフォルトとして採用され、2引数ならばそれぞれの値を用いて、このメソッドが呼ばれる。

このように、同じ名前で引数の型が異なるものの多重定義をオーバーロードという。

オーバーロードしているため、3引数の場合と4引数の場合は、こちらではなく、先ほど定義したメソッドが呼ばれる。

デフォルト引数を使わず、全く、引数の並びが違うような場合であれば、ほぼ完全にオーバーロードされた メソッドを区別できるが、引数によってそれが区別できない、あいまいな場合はコンパイルエラーとなる。

constメソッドと非constメソッドは明確にオーバーロードによって区別される。同じく、 引数のconst性もまたその区別の対象である。また、クラスの定義の時点ではあいまいさがなくとも、 引数に指定したオブジェクトのキャストによってあいまいさが生じる場合も考えられる。

コンストラクタも複数定義できるが、これは、コンストラクタがオーバーロード可能であるためである。 逆に引数をとらないデストラクタは、オーバーロード不能である。

オーバーロード自身は非常に便利であり、また不可欠なものであるが、その使い方にはある程度の慣れが必要である。


文章作成 : yukki-ts (-+-twilight serenade-+- [stage])