漸くクラスの説明に入ります。(漸くというのは、僕が怠けていてクラスの説明をなかなか書かなかったと言うことなのですが)
クラスは C 言語と C++ を分け隔てる一つの大きな特徴です。 また、オブジェクト指向と非オブジェクト指向の言語を分ける特徴と言うことも出来るでしょう。 (世の中にはクラスベースのオブジェクト指向ではなくて、プロトタイプベースのオブジェクト指向言語も存在しますが…。)
今迄のプログラムでは、int や double 等のデータが存在して、それに対して演算や命令などを作用させるという感じのプログラムの書き方をしてきました。 オブジェクト指向プログラミング (OOP; Object Oriented Programming) では、 データが存在して、更にそのデータに付随する操作を呼び出すと言う感じのプログラムの書き方をします。 この〈データとそれに付随する操作の集合〉をオブジェクトと呼びます。
まあ、机上で論じているより、早く中身を見た方が良いです。
ここの説明では、色々と抜けている所があるので、これだけでは不十分です。 でも、いきなり沢山のことをやっても分からなくなってしまうと思うので、先ずは茲でクラスの扱いについて慣れる様にして下さい。
プログラミングは何はともあれ書くことです。
クラスというのは簡単に言えば、新しい型を定義するということです。 今迄使ってきた方の中には、int や double 等の基本型やポインタ型などがありました。 クラスを用いれば、これらの既存の方を組み合わせて新しい型を作ることが出来ます。
クラスの定義を学ぶ前に型という概念について改めて確認をしてみます。
先ず、「型」とその型に属する「データ」について考えてみます。 型とデータは厳密に区別しなければならない物です。 型というのは分類であって、データというのは実際にその型に分類されるメモリ上のデータのことです。 この様なメモリ上に実際にあるデータのことを「インスタンス」と言います。 例えば、int というのは型であって 3 という値はインスタンスです。 「犬」という分類に対して「ポチ」という特定の犬が存在しているのと同じ感覚です。 因みに、変数はインスタンスを指し示す為の看板の様な物です。
更に、int を例にとって型という物が何であるかを再考してみます。 先ず、int 型は 4B のデータであって、二進で整数を表現する物です。 然し、それだけではプログラムは動きません。 例えば、int 型の変数を定義した時に初期化を行います。
int a=10; // これの動作は int の定義の内
また、int 型同士の間の演算が定義されていなければ為りません。
int a=3,b=5; a+b*a; // 演算も int の定義の内
更に、キャストの時の規則なども int 型としての性質です。 また、自分で定義した型には「型の持つ関数」の様な物も定義出来ます。
ここでは、複素数 complex number のクラスを作ってみることにします。 (実は標準ライブラリの中に既に複素数は用意されていたりする訳ですが、 他に簡単な題材がないので簡易複素数を作ります。)
先ずは、クラスを宣言しなければ為りません。 クラスの宣言は下の様にします。
class Complex{ // この中に色々なメンバを並べて記述 (後述) };
今回のクラスの名前は「Complex」です。 Complex でなくても自分の好きな名前を付けることが出来ます。 (と言っても既存の変数などと被る様な名前は付けられません。勿論) また、クラスの名前のことをタグ名と呼ぶことがありますが、これは多分 C 言語の名残です。
また、class Complex{}; の最後のセミコロンを忘れない様にして下さい。
一番重要なメンバは、メンバ変数と言われる物です。 データメンバ、フィールド等と呼ばれることもあります。 これは、インスタンスの「形」を規定するメンバです。 具体的に見ることにしましょう。
class Complex{ public: // ←この行は今は気にしないで下さい。その内にやります。 double real; // 実部を表すメンバ double imag; // 虚部を表すメンバ };
メンバ変数は普通の変数と同じように宣言します。 幾つでも好きな数だけ宣言することが出来ます。
上の様に、二つのメンバ変数を定義した時には、Complex のインスタンスは下の様な形になります。
double が 8B の場合には、それがくっついて出来た 16B の大きさのデータが Complex のインスタンスになります。
それぞれのメンバ変数を触りたい時には
#include <iostream> class Complex{ public: double real; double imag; }; int main(){ Complex a; // (1) a.real=1; // (2) a.imag=2; // 値の代入 std::cout<<a.real // 値の取得 <<" + i" <<a.imag <<std::endl; return EXIT_SUCCESS; }
int 型ならば変数を宣言した時に 10 という値を入れたかったら、
int a=10;
と指定することが出来ました。 然し、今の段階で Complex 型ではその様なことは出来ません。 初期化しようにも Complex のリテラルがないからです。 (と言って、残念ながらリテラルを定義することは出来ません。) 初期化する場合には、
Complex c={1.0, 2.0};
等とします。 今回の場合には一番目が real で二番目が imag に代入される形で初期化が行われます。 でもこの使い方は何番目が何番目のメンバに対応しているのか注意しなければ為りませんし、 分かりにくいので、使い捨ての構造体など以外では使用されません。
普通はコンストラクタを定義してそれで初期化を行います。
class Complex{ public: double real; double imag; // コンストラクタ Complex(double real,double imag){ this->real=real; // (1) this->imag=imag; // 動作確認用 (実用のクラスでは不要です) std::cout<<"ctor!"<<std::endl; } };
コンストラクタは一種の関数の様な物です。 コンストラクタの名前? は宣言したクラスと同じ名前 (Complex) でなければ為りません。 (或いは、Complex というのがコンストラクタの戻り値の型で、関数名はないのだという見方も出来ます。)
(1) では、《自インスタンスのメンバ変数 real》 に、《貰ってきたコンストラクタ引数の real》 の値を代入しています。 こうすれば、貰ってきた引数の値をその儘、自インスタンスの値とすることが出来ます。 また、もうお分かりの通り、自分のメンバに触る時には this-> に続けて記述します。
#include <iostream> class Complex{ public: double real; double imag; // コンストラクタ Complex(double real,double imag){ // 中略 } }; int main(){ Complex a=Complex(1,2); // (1) Complex b(3,4); // (2) return EXIT_SUCCESS; }
ctor! ctor!
コンストラクタが a の時と b の時で二回呼び出されたことが分かると思います。
また、コンストラクタの引数は自由です。 同じ引数を持たなければ、複数のコンストラクタを定義することも出来ます。 (つまり、コンストラクタのオーバーロードが可能と言うことです。)
コンストラクタと対になる物が、デストラクタです。 実は、デストラクタは、未だこの段階では使い道がありません。 然し乍ら、コンストラクタと対になると言うこと、 スコープの概念をより実感する為に有用であると思ったことなどからここで扱っておきます。
コンストラクタは変数を宣言した時などに呼び出されました。 一方で、デストラクタは変数のスコープが切れた時に自動的に実行されます。 (特に指定しなくても勝手に実行されます。)
class Complex{ public: double real; double imag; /* (中略) */ // デストラクタ ~Complex(){ // 動作確認用 std::cout<<"dtor!"<<std::endl; } };
デストラクタの定義はコンストラクタの時と似た様な感じで、~〈クラス名〉 として宣言します。 また、デストラクタは自動的に呼び出される為の関数なので、引数は常に空 (void) で無ければ為りません。 (従ってオーバーロードは出来ません。)
class Complex{ // 中略 }; int main(){ Complex a(1,2); std::cout<<"a: initialized"<<std::endl; { Complex b(3,4); std::cout<<"b: initialized"<<std::endl; } std::cout<<"b: out of scope"<<std::endl; return EXIT_SUCCESS; }
ctor! a:initialized ctor! b:initialized dtor! b: out of scope dtor!
一つ目の ctor が a のコンストラクタで、二つ目が b のコンストラクタです。 また、一つ目の dtor が b のデストラクタで、二つ目の dtor が a のデストラクタです。
特に、デストラクタの呼び出しを指定していないのに、自動的に呼び出しが為されていると言うことが分かると思います。 また、b のデストラクタに関しては b: out of scope のメッセージよりも前に破壊 (destruct) されていると言うことが分かります。 これは、b: out of scope の手前で b のスコープが終わっているので、その時点で b が破壊されると言うことを示しています。
メンバ関数 (メソッドとも言う) はそのクラスに属している関数のことです。 今迄扱ってきた int 型や double 型ではメンバ関数はありませんでしたので、これは新しい型の機能と言うことになりましょう。
メンバ関数の定義は、例えば以下の様にします。
class Complex{ /* 中略 */ void print(){ std::cout<<this->real <<" + i" <<this->imag <<std::endl; } };
上のメンバ関数は、自分の内容を標準出力に対して出力する関数になります。
#include <iostream> class Complex{ public: double real; double imag; /* 中略 */ void print(){ std::cout<<this->real <<" + " <<this->imag <<"i" <<std::endl; } }; int main(){ Complex a(1,2),b(3,4); a.print(); // インスタンス a について print メソッドを呼び出す b.print(); // インスタンス b について print メソッドを呼び出す return EXIT_SUCCESS; }
1 + 2i 3 + 4i
クラスでは、int や double で足し算などが実行できた様に、自分で演算を定義することが出来ます。 ここでは、足し算を例にして演算子を定義してみることにします。
加算の演算子を定義する時は、「operator+」という名前の関数を定義する様な雰囲気で行います。
class Comples{ /* 中略 */ Complex operator+(const Complex& r){ // (1) return Complex(this->real+r.real,this->imag+r.imag); } };
#include <iostream> class Complex{ ... Complex operator+(const Complex){ ... } }; int main(){ Complex a(1,2), b(3,4), c(-5,6); Complex d=a+b+c; // (1) d.print(); (d+c).print(); // (2) return EXIT_SUCCESS; }
-2 + 12i -7 + 18i
同様にして引き算や掛け算、割り算なども定義することが出来ます。
さて、先の節でクラスが大体どんな物なのかと言うことはお分かり頂けたと思います。 先程は、早く暮らすという物がどんな物なのかを把握して頂く為に適当に流してしまった所が沢山あるので、 茲では詳しくそれぞれについて述べる様にして一から説明し直そうと思います。
クラスは、「初めてのクラス」の節で説明した様に、型を定義する物です。 「初めてのクラス」で説明したことを纏めると以下の様になります。
また、クラスのインスタンスを、特にクラスインスタンス又はオブジェクトと呼びます。
型の内容に立ち入ってみると、型というのは以下の要素から為ります。
普段は、特にデータの部分だけを見がちですが、そのデータを扱うことが出来るのは、 その扱い方などの規則がその型に付随して来ればこその事です。
クラスでは、上に挙げた様な物を定義して新しい型を作り出すことが出来ます。 それぞれの要素は、クラスの中に複数のメンバとして定義されます。
クラスの宣言は以下の様な物になります。
class 〈クラスの名前〉{ /* クラスの中身 */ }; // ←セミコロンを忘れずに! // 例 class Complex{ };
今は、未だクラスを宣言しただけで中身を記述していないので、これは空のクラスです。 つまり、唯のクラスの「殻」を定義しただけなのです。 これからやっていく事は、このクラスの中に「メンバ」を複数定義していって、 クラスの定義を充実した物にすることです。
※ この〈クラスの名前〉の事を特にタグ名と呼びます。
クラスの宣言は、グローバルな場所 (== 一番外側; 関数の中でもクラスの中でもない所) にも宣言できますし、関数の中で宣言する事も出来ます。 また、クラスの中で更に入れ子でクラスを宣言するという事も出来ます。
関数の中で宣言したクラスをローカルクラスと呼びます。 然し、勿論ローカルクラスはその関数の中でしか使用する事は出来ませんので、使用する機会はそんなに無いと思います。
また、クラスの中に入れ子で定義したクラスをネストクラス (nested class) とか 内部クラス (inner class) 等と呼びます。 これは、親クラスの中で何か複雑な処理をしたい時の補助に使ったりします。 メンバアクセスを public にしておけば、親クラスの外側から使う事も可能ですが、 親クラスの中だけで使用する様な場合に用いる事が多いと思います。
// グローバルな場所 class ClassA{ ... // 入れ子のクラス class ClassB{ ... }; }; void some_func(){ // ローカルクラス class ClassC{ ... }; ... }
上の様にクラスを宣言した場合には、「class 〈クラスの名前〉」が型名となります。 然し、これは煩雑なので C++ では class の部分を省略して「〈クラスの名前〉」の部分だけで型名として機能します。
class Complex a; Complex b;
また、変数をクラスの定義と共に宣言する事も出来ます。 更に、そのクラスの変数をそれしか作らないのであれば、タグ名を省略する事も出来ます。
class{ ... } var; // var と言う変数の宣言
この場合には、或る意味 class{...} の部分が型名の様な働きをしていると言えます。
クラスを関数の引数として宣言する事も出来ます。
void func(Complex c){ ... // c を使用した処理 }
然し、一般的には此の様な使用方法は為されません。 クラスは、サイズ的に大きな物になりがちなので、 値渡しにすると無駄に沢山のメモリーコピーが生じてしまい計算を遅くする原因になってしまいます。 そこで、通常は参照渡しで引数を渡す事になります。
然し、お察しの通りそのまま参照で渡してしまうと、渡したインスタンスの内容を関数の中で変更されてしまう可能性が残ります。 そこで、「関数の中で渡された引数の内容を変更しない」という事を明示する為に、引数に const 修飾子を付ける事が普通です。 此の事は関数の中で誤って、引数を変更してしまうと言うバグを防ぐ為にも有効です。 (勿論、内容を変更することを目的としている関数では const を付ける必要はありません。)
void func(const Complex& c){ ... // c を使用した処理 }
所で、クラスにも関数と同様にプロトタイプを定義することが出来ます。
// プロトタイプ class Complex; int main(){ ... // Complex を使用したコード } // 実装 class Complex{ ... };
プロトタイプは、クラスの定義をファイルの後の方に書きたい場合や、 複数のクラスの間で互いに利用し合う場合などに用います。
メンバアクセスというのは、それぞれのメンバをクラスの外から触ることが出来るかどうかを指定する物です。 public と private 及び protected の三種類があります。 ここでは public と private について説明します。 (protected はもっと先で出てきます。)
それぞれのメンバが private になるか public になるかは、 private: や public: の指定によって決まります。 その規則は例えば下の様になります。
class Class1{ // 何も指定していない所は private double x; // ←private double y; // ←private public: // この指定以降は public Class(){ ... } // ←public ~Class(){ ... } // ←public private: // private 指定があったら、それ以降はまた private void foo(){} // ←private void bar(){} // ←private public: ... // public な領域 ... private: ... // private な領域 ... ... };
また、同じアクセス指定を連続して行っても構いません。 (その場合は、二つ目以降の指定は論理的には何の役割もしていません。 然し、特にそのメンバのアクセスがこれであると言う事を分かり易くしたい場合や、 クラス内での論理的な区切れ目には敢えてメンバアクセスを指定する事があります。)
class Class2{ public: ... public: ... };
private の様に制限を加えるだけの事に疑問を持つ人がいるかも知れません。 確かに、private はクラスの使い方に制限を加えるだけであって、何らの新しい機能を提供する訳でもありません。 然し、この機能はオブジェクト指向に於いて重要な役割を果たします。 この private の機能を特に隠蔽等と呼ぶ事があります。
オブジェクト指向では、オブジェクト (クラスインスタンス) を一つの洗練された「道具」の様にして扱います。 良い道具というのは、使う時にはその使い方を知れば良いだけであり、中身についての知識を要求しない様な物です。 例えば、パソコンの場合でもその設計の仕方など知らなくても、 簡単に (パソコンを設計するのよりは簡単に) 使う事が出来ます。
自分でクラスを作成する場合にも、それを "良い道具" とする事が大切です。 「自分で書くのだから中身についての知識は持っているのだ」等と侮っては行けません。 例えば、一年前に自分の書いた計算用紙が何処からか出てきた場合、それを見て何を計算していたのか思い出すのに苦労すると思います。 プログラムでも、「一週間前に自分の書いたクラスのコードを読んで、何を考えてその様にコードを記述したのか思い出すのに苦労する」という事は良くある事です。 その時に、書いたクラスが "良い道具" になっていれば、使う度にクラスの中身のコードについて思い出すと言う事をせずに済みます。
では、クラスが "良い道具" であるとは具体的にはどの様な事なのでしょうか? 勿論、その要件には色々あると思いますが、茲では「隠蔽」の観点から見てみる事にしましょう。 簡単に言ってしまうと、「public なメンバが少ない」という事です。 (「少ない」というのは、そのクラスの機能の割に少ないという事であって、機能がないと言う事では勿論ありません。)
この様に設計する事の利点を具体的に考えてみます。
private なメンバを使用しない場合、自由気儘にクラス内のメンバを外から触ってプログラムを書く事が可能になります。 そうすると、クラスの外と中とで複雑な関係性を持たせる事が可能で、スパゲッティコードの原因となります。 意図してスパゲッティなコードを書こうと思わなくても、 「そのオブジェクトに一任するよりは少し弄って処理した方がいいや」等という微妙な効率を考えてしまう事があります。 この様な事が蓄積すると直ぐにスパゲッティコードが誕生してしまいます。
そもそも、クラスを導入したのは、或る一定の処理を一纏めにしてプログラムを論理的に整理する為です。 それなのに、クラスの中身に外部から介入していては、クラスにした意味がありません。 稍もすれば、クラスなど使わずに書いたのと同じになってしまいます。
クラスを作る時には、微妙な効率等は全て切り捨てて、処理の分離に徹しなければ為らないのです。 積極的にメンバを private にする事が処理の分離の助けになるでしょう。 (元々、微妙な効率を考えても大抵効果がありません。 実行時間の短縮に比べると、微妙な効率を考える事に依るコーディング時間の伸長の方が格段に長くなるでしょうから)
クラスの機能を少数の public メンバに集約する事さえ考えていれば、 他の事は何も考えなくても自然にクラスの機能が分かり易く纏められます。 逆に全て public にしているとメンバに論理的なまとまりが無くなってしまうかも知れません。
"良い道具" というのは、通常の使用で簡単に壊れては行けませんし、多少変な使い方をしても大丈夫でなければ為りません。 その為には、例えば、メンバ関数を定義する際にあらゆる引数の可能性 (明らかに誤っている様な引数の可能性も含む) を考えて設計するなどの事が必要です。
然し、すべてのメンバが public であると、その様な可能性を考えて設計する事はとても困難か不可能になってしまいます。 一方で少数のメンバが public に為っていれば、public なメンバだけはあらゆる使い方に対して頑健である様に設計するという事が容易になるでしょう。
まあ、private に対する主義主張はともかくとして、結局言いたいのは、外に公開しなくても良い様な物は private にする/ 外に公開したい物が沢山ある場合でも、出来るだけ公開する物が少なくなる様に工夫するという事です。
メンバ変数は、先程も説明した様に、その型のデータを規定するメンバです。 データメンバ、フィールドなどといった言い方も為されます。
宣言の仕方は、丁度関数内で変数を宣言するのと同様に、以下の様になります。
class Complex{ 〈型名〉〈変数名〉; }; // 例 class Complex{ double real; double imag; };
メンバ変数の名前は、通常の変数と同じようにアルファベットとアンダースコア・数字などを組み合わせて表現します。 更に、クラス内で重複がない限り自由な物を選べます。 つまり、グローバル変数やグローバル関数で同じ名前の物があっても、クラス内で同じ名前の物を宣言することが出来ると言うことです。 (但し、クラス内で同じ名前のメンバ変数とメンバ関数を定義したりと言うことは勿論出来ません。)
また、上の例にある通り、メンバ変数は複数定義することが出来ます。 上の例ではどちらも double 型になっていますが、色々な型を混合してメンバにすることも勿論可能です。 また、別のクラスの変数をメンバにすることも可能です。
class ClassA{ int i,j,k; }; class ClassB{ double a; int* b; ClassA c; // クラスをメンバに };
但し、自分自身と同じ型の変数をメンバにすることは出来ません。 つまり、自分を定義するのに自分を使うことは出来ないということです。
class Class1{ Class1 a; // NG };
メンバ変数を使用する時には、「.」 演算子を用います。具体的には以下の様になります。
〈式〉.メンバ名
但し、〈式〉はクラス型の値を返す式で、メンバ名はそのクラス型に属するメンバの名前です。
class Class2{ public: int index; }; int main(){ Class2 v; // 値の設定 v.index=1; // 値の取得 std::cout<<v.index<<std::endl; int x=v.index; }
コンストラクタは、変数の初期化時に実行される関数の様な物です。 例えば、変数を宣言するのと同時にその値を設定する場合や、新しいインスタンスを作成する場合に使用します。 new 演算子で作成したインスタンスにも適用されます。
また、逆にデストラクタは変数の寿命が尽きる時 (スコープの外に出る時) に自動的に実行されます。 また、動的に確保したインスタンスを delete する際にも適用されます。 今の所は、このデストラクタの有り難みは分からないかも知れませんが、その内に重要になってきます。
コンストラクタの宣言は以下の様にして行います。
class 〈クラス名〉{ 〈クラス名〉(〈引数リスト〉){ /* コンストラクタの中身 */ } };
これは、通常の関数とは異なった宣言の仕方で、戻り値と関数名を指定していた部分に、〈クラス名〉が来ています。 とは言っても、それ以外の部分は関数と同じです。オーバーロードを作成することも出来ます。
class Complex{ double real,imag; public: Complex(double real,double imag){ this->real=real; this->imag=imag; } Complex(double real){ this->real=real; this->imag=0; } Complex(){ this->real=this->imag=0; } };
三つ目のコンストラクタの様に、引数を持たないコンストラクタのことを そのクラスのデフォルトコンストラクタ、既定のコンストラクタなどと呼びます。
アグリゲートというのは、無理矢理内意方をすれば {} による初期化子を用いる事が出来る様な型の事です。 例えば、配列などがアグリゲートの一種です。
int arr[10]={1,2,3,4,5}; // 配列はアグリゲート
また、アグリゲートは初期化を明示的にする必要もありません。
int arr[10];
《一つもコンストラクタの宣言されていないクラス》も、アグリゲートの一種です。 つまり {} による初期化子を使用する事が出来ます。
class ClassC{ double val; const char* msg; int num; }; ClassC x={3.14, "Hello", 1};
また、アグリゲートなクラスの変数は、宣言時にコンストラクタを要求しません。
class Aggregate{ ... }; Aggregate a;
然し、一つでもコンストラクタを定義してしまうとそのクラスはアグリゲートではなくなってしまいます。 従って、初期化子は使えなくなりますし、対応するデフォルトコンストラクタが必要になってしまいます。
class NonAggregate{ int x; int y; public: NonAggregate(int){} }; NonAggregate m={4,5} // (1) ERROR NonAggregate n; // (2) ERROR
使うのは変数を宣言する場合などです。宣言の際に自動的にコンストラクタが呼び出されるのです。
int main(){ Complex a(1,2); // 一つ目のコンストラクタ Complex(double,double) で初期化 Complex b(3); // 二つ目のコンストラクタ Complex(double) で初期化 Complex c; // 三つ目のコンストラクタで初期化 }
また、変数の初期化でなくても、式の中でコンストラクタを呼び出してそのクラスの値を作り出すことが出来ます。
Complex a= Complex(1,2) + Complex(4,5);
更に、new で動的にインスタンスを確保した際にもコンストラクタが呼ばれます。
int main(){ Complex* p=new Complex(1,2); ... delete p; return 0; }
つまり、領域を確保すると同時に初期化を行う事が出来るのです。
引数が一つしかないコンストラクタは暗黙的キャスト演算の定義にもなっています。 上の Complex の場合で言えば、二つ目のコンストラクタ Complex(double) は double から Complex への暗黙的キャストの定義に為っています。
int main(){ Complex a=3.0; // OK: Complex(double) が実行されて Complex に変換されてから代入される }
然し、暗黙的にキャストされると困るという状況も存在します。 例えば、ベクトルのクラスのコンストラクタとして、Vector(int jigen) を作ったとします。 この時、これは int から Vector への変換になっています。 と言うことは、プログラムを少し書き間違えただけで、勝手に整数がベクトルに化けてしまう様なプログラムが出来上がってしまう可能性が出て来ます。 そこで使用されるのが、暗黙的な変換を避ける為のキーワード explicit です。
class Vector{ ... explicit Vector(int n){ ... } };
これで、いつの間にかに整数がベクトルに化けていたという現象は起こらなくなるでしょう。 但し、明示的に変換を行えば何時でも整数をベクトルに変換することが出来ます。 (explicit というのはここでは「明示的に」と言う意味です。)
デストラクタの宣言は、次の様に宣言します。
class 〈クラス名〉{ ... public: // デストラクタ ~〈クラス名〉(){ /* デストラクタの中身 */ } }; // 例 class Complex{ ... ~Complex(){ ... } }
デストラクタに引数を指定する事は出来ません。 それは、クラスインスタンスが破壊される時に、自動的に呼び出される物であって引数は渡されない為です。 従って、オーバーロードも作成する事が出来ません。無引数のデストラクタを一つしか定義できません。
デストラクタの定義は省略することが可能です。 省略した場合には、デストラクタの呼び出しは為されません。
また、デストラクタを定義する場合には、public で定義しなければ為りません。 public でないと、インスタンスを破壊しようとした時にアクセス出来ないというエラーが起きてしまいます。
デストラクタはコンパイラによって自動的に呼び出しが追加されます。 では、デストラクタはどのような状況で呼び出されるのでしょうか? デストラクタが呼び出される状況には、具体的には色々あります。 それに就いてみていく事としましょう。
変数の寿命が尽きるときに、その後始末としてデストラクタが呼ばれます。 (此の事を逆手に取れば、変数の寿命を調べる事が可能になります。)
ローカル変数の寿命が尽きるのは、その変数のスコープが終わるときです。
int main(){ Complex a(1,2); { Complex b(3,4); ... // ←ここで b のデストラクタが呼ばれる。 } return 0; // ←ここで a のデストラクタが呼ばれる。 }
また、グローバル変数や静的ローカル変数 (及び、後述の) の寿命が尽きるのは、プログラムが終了するときです。 つまり、main 関数から抜けた後に実行されます。
#include <iostream> class Test{ public: Test(){ std::cout<<"ctor"<<std::endl; } ~Test(){ std::cout<<"dtor"<<std::endl; } } test; int main(){ std::cout<<"main"<<std::endl; return EXIT_SUCCESS; }
ctor main dtor
メンバ変数の寿命が尽きるのは、そのメンバを保持している親クラスのインスタンスが削除されるときです。 簡単に言うと、メンバのデストラクタが呼び出されてから親のデストラクタが呼び出されます。 (親にデストラクタが定義されていない時は、メンバのデストラクタだけでも呼び出されます。)
class Class1{ public: Class1(){std::cout<<"class1 ctor"<<std::endl;} ~Class1(){std::cout<<"class1 dtor"<<std::endl;} }; class Class2{ Class1 mem; public: Class2(){std::cout<<"class1 ctor"<<std::endl;} ~Class2(){std::cout<<"class1 dtor"<<std::endl;} };
class1 ctor class2 ctor ←メンバ mem のコンストラクタ main class2 dtor ←メンバ mem のデストラクタ class1 dtor
class Complex{ public: ... Complex operator+(const Complex& r){ ... } } int main(){ Complex a(1,2); Complex b=a+a+a; return EXIT_SUCCESS; }
上の a+a+a の部分では、先ず a+a が計算されてから (a+a)+a が計算されます。 この a+a の計算結果の Complex が一時的にメモリ上に生じます。 これを部分式の評価結果と言います。 この部分式の結果は、(a+a)+a の計算が終了した時点で要らなくなります。 その時点で、この部分式の結果を破壊しなければ為りません。
つまり、部分式の評価が終わって不要になった時点でデストラクタが呼ばれるのです。
先程、new で動的にインスタンスを確保した際に初期化が行われるという事を述べました。 と言う事は、delete の時にデストラクタが呼ばれるという事は想像に難くないと思います。 実際に delete を実行すると、デストラクタが呼び出されて、その後で領域が開放されます。
デストラクタは明示的に呼び出すことも出来ます。 (コンストラクタは明示的に呼び出すことは出来ません。)
但し、明示的に呼び出したからと言って、そこで勝手に変数のスコープが切れたり、 メモリが解放されたりと言うことはありません。 また、明示的に呼び出した場合には、本当にインスタンスの寿命が切れた時にも呼び出されることになりますので、 一つのインスタンスに対して複数回デストラクタが呼び出されることにも為りますので注意して下さい。
class Class{ ... } int main(){ Class c; c.~Class(); // ←明示的なデストラクタ呼び出し return 0; // ←暗黙的なデストラクタ呼び出し }
クラスのメンバには関数を定義する事も可能です。これを、メンバ関数と言います。 メソッドなどと呼ばれる事もあります。 これは、オブジェクト固有の機能・性質を関数として実装する場合に用いられる物です。
メンバ関数は、通常の関数と同じ様な書き方でクラスの内側に記述します。
class Class1{ public: 〈戻り値の型〉〈関数名〉(〈引数の型〉){ // 関数の処理の中身 } }; // 例 class Complex{ public: void print(){ std::cout<<this->real <<" + " <<this->imag <<"i" <<std::endl; } };
メンバ関数の中では、this-> に続けて自分自身のメンバにアクセスすることが可能です。 (説明していませんでしたが、コンストラクタやデストラクタの中でも同様です。)
class Sample{ int m_val; void sample(int i){ // メンバ変数にアクセス this->m_val; // メンバ関数にアクセス if(i)this->sample(0); // (1) // デストラクタを強制的に呼び出し this->~Sample(); } }
また、メンバ関数の中で自分自身を変更しない (則ち、メンバ変数の値が変化する様な操作を行っていない) 場合には、 その事を意思表示することが可能です。
class Class2{ ... int method() const{ // ← ... // 自分自身を変更しない様なコード } };
つまり、引数リストの終わりの ')' と、本体の開始の '{' の間に const と記述します。
自分自身を変更しない様なメンバ関数には、出来るだけ必ずすべてこの指定を行う様にして下さい
(中途半端に const を使用しているとコンパイル出来なくなる状況が存在します)。
メンバ関数にアクセスするには、メンバ変数にアクセスした時と同様に . を使用します。
int main(){ Class2 a; std::cout<<a.method()<<std::endl; return 0; }
演算子は、そのクラス同士の演算、または、そのクラスと他の型の間の演算を定義を行う為の物です。 定義出来る演算子には、int や double 等の基本型で使えた演算子と、キャスト演算子などがあります。
定義出来る演算子は予め決まっています。 勝手に新しい演算子を創成出来るというわけではないので注意して下さい。 飽くまで、既存の演算子に対するオーバーロードなのです。 (若し仮に新しい演算子を定義出来たとしても、演算子の優先順位などで問題が生じるでしょう…)
(また、既存の演算子の中にも定義出来ない物が存在します。)
定義出来る演算子の一覧
// 単項演算子 operator+ // 正号 operator- // 負号 operator* // 間接参照 operator& // アドレス取得 operator~ operator! operator++ (前置) operator++ (後置) operator-- (前置) operator-- (後置)
// 二項演算子 operator+ operator- operator* operator/ operator% operator| operator& operator^ operator<< operator>> operator|| operator&& operator, operator!= operator== operator< operator> operator<= operator>=
// 代入演算子 operator= operator+= operator-= operator*= operator/= operator%= operator|= operator&= operator^= operator<<= operator>>=
単純な代入演算子は、特に定義していない場合にはデータのコピーとして扱われます。 また、複合の代入演算子では、特に定義していない場合には、例えば a+=b を a=a+b と解釈する様に動作します。 (勿論 + 演算子を定義していない場合には使えませんが…)
また、複合演算子 += を定義する場合には、特に + を定義しなければならないと言うことはありません。 (但し勿論 += の形でしか使用出来ないと言うことになります。) 他の演算子についても同様です。
// 他 operator 〈型名〉// キャスト演算子 operator-> // アロー演算子 operator[] // 添字アクセス operator() // 関数呼び出し operator new operator new[] operator delete operator delete[]
演算子は、クラスの中にメンバとして定義することも可能ですし、 グローバルな場所に記述することも可能です。
例えば、クラスの中に記述する時には以下の様にします。
class Complex{ ... public: // (1) Complex operator+(const Complex& r) const{ return Complex(this->real+r.real,this->imag+r.imag); } };
(内側でしか使わない様な演算子を定義するのであれば、public にする必要はありません。)
また、グローバルな場所に定義しておく場合には次の様にします。
class Complex{ ... friend Complex operator+(const Complex& l,const Complex& r); // (2) }; Complex operator+(const Complex& l,const Complex& r){ return Complex(l.real+r.real,l.imag+r.imag); }
※ 演算子の中から public なメンバにしかアクセスしないというのであれば、friend の指定は必要ありません。
何れの場所に記述する場合でも、戻り値の型や引数の型の指定の方法は演算子によって異なるので、一つ一つ見ていかなければ為りません。
演算子をオーバーロードする時には、意味的に分かり易い様な機能を演算子の処理に割り当てなければ為りません。 何でもかんでも演算子にするのは良くありません。一々説明しなくても分かるぐらいの機能を演算子にするべきです。 (更に、+ 演算子で引き算を行い - 演算子で加算を行うという様な実装の仕方は言語道断です。 バグ発生源になることが目に見えています。)
直感的に演算によって中身が変化しないと思われる様な演算子では、中身を変更しない様に設計します。 例えば二項演算子 + の場合、「足し算をしたら元の変数の値が変わってしまった」というのでは困ります。 クラスの使い手が + で加算を行う時は、演算の引数の中身が変わるとは通常思わないからです。
それから、関連する演算子は纏めて定義しましょう。 例えば、+ をオーバーロードしたのに - をオーバーロードしていないという様な状況は作らない様にするべきであると言うことです。 + がオーバーロードされている様な場合には、そのクラスの使用者は - もオーバーロードされているだろうと (無意識に) 予想してコードを書くかも知れないからです。
/* 一括で定義するべき物 */ // 符号 operator+ operator- // 加減 operator+ operator- // 乗除 operator* operator/ // 比較 operator== operator!= // インクリメント・デクリメント operator++ operator-- operator++ // 後置 operator-- // 前置 // 順序 operator> operator< operator>= operator<= // メモリ確保 operator new operator delete operator new[] operator delete[] // 複合の代入演算子についても同様 (略)
重い処理は演算子にするのではなくて、関数にして下さい。 演算子にすると記述が楽な為、重い処理を中でして居ると分かっていてもついつい演算子を沢山呼び出してしまいがちです。 演算子というのは、本来は単純に評価結果を返す為の物であって、何かの仕事をする為の物ではないのです。
演算子は、他の int や double に対して記述するのと同じようにして使用する事が出来ます。
int main(){ Class a; a+a*b; return 0; }
また、通常の関数の様に呼び出す事も出来ます。 が、これでは通常の関数として定義するのと同じなので、演算子の恩恵を受ける事は出来ません。 余り使用されません。
int main(){ ClassA a; a.operator+(a); // メンバとして宣言している時 ClassB b; operator+(b,b); // グローバルな所に宣言している時 return 0; }
各々の演算子のオーバーロードの仕方についての詳細は、オーバーロードしたくなった時に以下を見ることにして下さい。
静的メンバは始めの説明ではしませんでしたが、よく使われるので茲で紹介します。 静的メンバには、静的メンバ変数と、静的メンバ関数があります。 静的メンバ関数の定義は、通常の定義に static を付けるだけです。 静的メンバ変数は、通常の定義に static を付ける事で宣言出来ます。 但し、クラス内に宣言しただけでは宣言しただけに過ぎませんので、 使う為には何処かのファイルのグローバルな所に実体を定義しなければ為りません。
class 〈クラス名〉{ ... // 静的メンバ変数 static 〈型名〉 〈変数名〉; // 宣言 // 静的メンバ関数 static 〈戻り値〉 〈関数名〉(〈引数リスト〉){ ... } }; // 静的メンバ変数の定義 〈型名〉〈クラス名〉::〈変数名〉; // 定義 //---------- 例 ---------- class Complex{ ... public: static Complex ImagUnit; static Complex PureImaginary(double imag){ return Complex(0,imag); } }; Complex Complex::ImagUnit(0,1);
静的メンバはクラスのインスタンスに属しているのではなくて、 クラス自体に属している変数や関数のことです。 静的メンバについては…もっと色々難しく言うことも出来るのかも知れませんが、 動作としては、グローバルに変数や関数を於いておくのと大して変わりません。
更に、特定のインスタンスに関連して存在している物ではないので、 静的メンバ関数の中では 「this ポインタで何処かのインスタンスの中身を触る」といったことは出来ません。
利点としては、 その変数・関数がそのクラスに関連した物であると言うことを意識してプログラムを書けると言うこと、 メンバアクセスを制限出来るという事等があると思います。 そのクラスの中からしか使わない様な変数・関数や、 そのクラスに関連のある変数・関数は静的メンバとして実装する様にしましょう。
静的メンバはクラスに属する物なので、:: 演算子を介してクラス名から触る事が出来ます。
class ClassA{ public: static int value; static void f(){ std::cout<<"This is a static-function"<<std::endl; } }; int ClassA::value; int main(){ // 静的メンバ変数 ClassA::value=1234; // 設定 int a=ClassA::value; // 取得 // 静的メンバ関数呼び出し ClassA::f(); return 0; }
また、それぞれのインスタンスに属するメンバの様に触る事も可能ですが、 この使い方は意味的に混乱を招く様な気がするので、止めた方がよいと思います…。
class ClassA{ public: static int value; static void f(){ ... } } a; int ClassA::value; int main(){ // 静的メンバ変数に代入 (a のインスタンスの内容自体は変更無し) a.value=100; // 静的メンバ関数の呼び出し a.f(); }