C++言語解説:2-2.演算子オーバーロード
2002-07-14 |
[C++ Index Top] [Prev] [Next] |
[概要] クラスでは演算子を独自に実装することができます。この演算子関数、フレンド 演算子関数を学習します。代入演算子とコピーコンストラクタの関係についても 理解します。 [構成]・フレンド関数 * フレンド関数とは ・演算子オーバーロード * クラスの演算子を定義する * メンバ関数による2項演算子オーバーロード * メンバ関数による単項演算子オーバーロード * フレンド関数による演算子オーバーロード * 代入演算子関数とコピーコンストラクタ ・その他の演算子オーバーロード * []と()演算子 ●フレンド関数 ◆フレンド関数 ・クラスのprivateメンバにアクセスできるのは原則として、クラスのメンバ関数のみ ですが、friendキーワードを付けることで外部の関数からprivateメンバへアクセス できるようになります。 [List1.フレンド関数の例] ┌───────────────────────────────────┐ #include <iostream> using namespace std; class Sample { int mi_sample; public: Sample(){ //コンストラクタ mi_sample=0; } int GetVal() { return mi_sample; } friend void add(Sample& obj); //フレンド関数プロトタイプ宣言 }; void add(Sample& obj) //フレンド関数 { obj.mi_sample++; //プライベートメンバへアクセス return; } int main() { Sample sample_a; cout << sample_a.GetVal() << endl; add(sample_a); //フレンド関数 cout << sample_a.GetVal() << endl; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] 0 1 └───────────────────────────────────┘ ・フレンド関数は、クラスのメンバ関数ではありません。よってメンバにアクセスす る際オブジェクト名を省略できません。 ・メンバ関数はプライベートメンバへアクセスできますが、これは見方を変えると 「何でもあり」になってしまいます。折角アクセス指定子で隠蔽しても意味が無く なってしまいます。悪用するとフレンド関数は間違いなく「毒」になります。 ・筆者はフレンド関数を、この後説明する「演算子オーバーロード」のために用意さ れた機能だと考えています。それ以外の用途では滅多に登場しないものであり、使 わざるを得ない場合は、対象クラスの機能について再度分析すべきでしょう。 ●演算子オーバーロード ◆クラスに演算子を定義する ・C++ではクラスに対し、演算子の意味を独自定義することができます。例えばList2 を見て下さい。 [List2.Basketクラス] ┌───────────────────────────────────┐ class Basket { int mi_apple; int mi_orange; public: Basket(int i_apple, int i_orange){ mi_apple = i_apple; mi_orange = i_orange; return; } Basket(){ mi_apple = 0; mi_orange = 0; return; } void AddApple(int i_add){ mi_apple += i_add; return; } void AddOrange(int i_add){ mi_orange += i_add; return; } int GetApple(){ return mi_apple; } int GetOrange(){ return mi_orange; } }; └───────────────────────────────────┘ ・このようなBasketクラスで2個のオブジェクトを定義してメンバのappleとorangeを 合わせる場合、通常はList3のようにするでしょう。 [List3.Basketクラスの利用例] ┌───────────────────────────────────┐ int main() { Basket A(2,1), B(3,2); A.AddApple(B.GetApple()); A.AddOrange(B.GetOrange()); cout << "Apple: " << A.GetApple() << endl; cout << "Orange: " << A.GetOrange() << endl; return 0; } └───────────────────────────────────┘ ・しかし「オブジェクトBの中身をAに入れる」という操作は、できれば A = A + B; と直感的表現をしたいものです。ところがC++では演算子関数(operator function) のオーバーロードを利用することで、この機能を実現できます。 ・演算子関数は以下のように定義します。 型 クラス名::operator演算子(引数リスト){・・・} では、実際の例を見てみましょう。 ◆メンバ関数による2項演算子オーバーロード ・List2に示したBasketクラスに演算子をオーバーロード実装します。今考えている演 算は A = A + B; なので、ターゲットとなる演算子は「+」と「=」です。List4に実装例を示します。 [List4.メンバ関数による演算子オーバーロード] ┌───────────────────────────────────┐ #include <iostream> using namespace std; class Basket { int mi_apple; int mi_orange; public: : : //省略 : //他のメンバ宣言/定義はList4と同様 : Basket operator+(Basket obj); Basket operator=(Basket obj); }; Basket Basket::operator+(Basket obj) { Basket tmp; tmp.mi_apple = mi_apple + obj.mi_apple; tmp.mi_orange = mi_orange + obj.mi_orange; return tmp; } Basket Basket::operator=(Basket obj) { mi_apple = obj.mi_apple; mi_orange = obj.mi_orange; return *this; } int main() { Basket A(2,1), B(3,2); A = A + B; cout << "Apple: " << A.GetApple() << endl; cout << "Orange: " << A.GetOrange() << endl; return 0; } └───────────────────────────────────┘ ・メンバ演算子関数は、演算子の左側にあるオブジェクトから呼び出されます。演算 子関数の引数は、右側にあるオブジェクトです。 ・「+」の演算子関数では、ローカルオブジェクトに呼び出し側オブジェクトと引数オ ブジェクトの合計結果を代入しています。演算段階ではオペランドのデータは変化 しないことを考慮しています。 ・「=」の演算子関数では引数オブジェクトの値を、呼び出しオブジェクトに代入し、 戻り値を自分自身としています。thisをは呼び出しオブジェクト自身を指すポイン タです。メンバ関数内ではメンバ変数/関数にアクセスする際、オブジェクト名を省 略できますが、省略しない場合は this->mi_apple; の表記となります。 ・thisポインタは代入演算子関数を作成するのに欠かせない存在です。thisポインタ は演算子オーバーロードのために用意された表記と考えられます。 ◆メンバ関数による単項演算子オーバーロード ・メンバ演算子関数により、「++」や「--」等の単項演算子をオーバロードすること ができます。 ・単項演算子は前置きと後置きがあります。それぞれ以下のように宣言します。 前置き: 戻値型 クラス型::operator演算子(){・・・} 後置き: 戻値型 クラス型::operator演算子(int){・・・} 後置き型の引数intは判別のために用意するダミーで意味はありません。 ・ではList5でインクリメント(前置き/後置き)を実装してみます。 [List5.単項演算子オーバーロード例] ┌───────────────────────────────────┐ #include <iostream> using namespace std; class Basket { int mi_apple; int mi_orange; public: : : //省略 : //他のメンバ宣言/定義はList4と同様 : Basket operator++(); //前置き Basket operator++(int); //後置き }; : : //省略 : //List4と同様 : Basket Basket::operator++() { mi_apple++; mi_orange++; return *this; } Basket Basket::operator++(int) { Basket tmp = *this; mi_apple++; mi_orange++; return tmp; } int main() { Basket A(2,1), B(3,2); A = A + B; B = ++A; cout << "B-Apple: " << B.GetApple() << endl; cout << "B-Orange: " << B.GetOrange() << endl; B = A++; cout << "B-Apple: " << B.GetApple() << endl; cout << "B-Orange: " << B.GetOrange() << endl; cout << "A-Apple: " << A.GetApple() << endl; cout << "A-Orange: " << A.GetOrange() << endl; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] B-Apple: 6 B-Orange: 4 B-Apple: 6 B-Orange: 4 A-Apple: 7 A-Orange: 5 └───────────────────────────────────┘ ・前置き演算子では、オブジェクトにインクリメント演算を行い、その結果を戻して います。後置き演算子では、インクリメント演算前の結果を戻しています。 ◆フレンド関数による演算子オーバーロード ・「*」の演算子定義を考えてみます。この場合 A * 2 及び 2 * A の結果は同じになるのが普通です。しかし今まで使用したメンバ関数による演算子 定義の場合 2 * A がうまく処理できません。こんなときはフレンド関数による演算子定義を行います。 ・フレンド演算子関数の場合、引数には両方のオペランドを指定します。この例では Basket operator*(int i_op, Basket obj){・・・} となります。List6に実装例を示します。 [List6.フレンド演算子関数の例] ┌───────────────────────────────────┐ #include <iostream> using namespace std; class Basket { int mi_apple; int mi_orange; public: : : //省略 : //他のメンバ宣言/定義はList5と同様 : Basket operator*(int i_op); //Basket * int friend Basket operator*(inti_op, Basket obj); //int * Basket }; : : //省略 : //List5と同様 : Basket Basket::operator*(int i_op) { Basket tmp; tmp.mi_apple = mi_apple * i_op; tmp.mi_orange = mi_orange * i_op; return tmp; } Basket operator*(int i_op, Basket obj) { Basket tmp; tmp.mi_apple = obj.mi_apple * i_op; tmp.mi_orange = obj.mi_orange * i_op; return tmp; } int main() { Basket A(2,1), B(3,2); A = A + B; A = A * 2; cout << "A1-Apple: " << A.GetApple() << endl; cout << "A1-Orange: " << A.GetOrange() << endl; A = 2 * A; cout << "A2-Apple: " << A.GetApple() << endl; cout << "A2-Orange: " << A.GetOrange() << endl; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] B-Apple: 6 B-Orange: 4 B-Apple: 6 B-Orange: 4 A-Apple: 7 A-Orange: 5 └───────────────────────────────────┘ ◆代入演算子関数とコピーコンストラクタ ・前回の「クラス」で、動的メモリ要素を持つオブジェクトを暗黙のコピー動作に対 応させるためにはコピーコンストラクタが必要であることを説明しました。 ・しかし「=」演算子を使用した場合、コンストラクタは動作しないため、動的メモリ 領域が共有されてしまいます。これに対応するため「=」演算子関数の中で別メモ リ確保を行ってみます。 [List7.代入演算子関数の例 (実行禁止!!)] ┌───────────────────────────────────┐ #include <iostream> using namespace std; class MBasket { int* mpi_n; int mi_index; public: MBasket(int i_index); ~MBasket(){ cout << "デストラクタ\n"; delete[] mpi_n; return; } void Add(int i_index, int i_num){ *(mpi_n+i_index) = *(mpi_n+i_index) + i_num; return; } int GetNum(int i_index){ return *(mpi_n+i_index); } MBasket operator=(MBasket& obj); //「=」演算子関数 }; MBasket::MBasket(int i_index){ cout << "コンストラクタ\n"; mi_index = i_index; mpi_n = new int [mi_index]; for (int i=0; i<mi_index; i++){ *(mpi_n+i) = 0; } return; } MBasket MBasket::operator=(MBasket& obj) { delete[] mpi_n; mi_index = obj.mi_index; mpi_n = new int[mi_index]; for(int i=0; i<mi_index; i++){ *(mpi_n+i) = *(obj.mpi_n+i); } return *this; } int main() { MBasket A(2), B(2); B.Add(0,5); B.Add(1,4); A = B; cout << A.GetNum(0) << endl; cout << A.GetNum(1) << endl; return 0; } └───────────────────────────────────┘ ・List7は実行することができません。その理由ですが、「=」演算子関数でthisポイ ンタの中身を戻した際に、デストラクタが動いてしまうからです。 ・結局のところ、「=」によるコピー動作ではコンストラクタの動かないことが問題で したが、そのために定義した「=」演算子関数を完全に動かすには、コピーコンスト ラクタが必要なのです。 [List8.代入演算子関数+コピーコンストラクタの例] ┌───────────────────────────────────┐ #include <iostream> using namespace std; class MBasket { int* mpi_n; int mi_index; public: MBasket(int i_index); MBasket(const MBasket& obj); //コピーコンストラクタ : : //省略 : //他のメンバ宣言/定義はList7と同様 : }; : : //省略 : //List7と同様 : MBasket::MBasket(const MBasket& obj){ cout << "コピーコンストラクタ\n"; mi_index = obj.mi_index; mpi_n = new int[mi_index]; for(int i=0; i<mi_index; i++){ *(mpi_n+i) = *(obj.mpi_n+i); } return; } MBasket MBasket::operator=(MBasket& obj) { delete[] mpi_n; mi_index = obj.mi_index; mpi_n = new int[mi_index]; for(int i=0; i<mi_index; i++){ *(mpi_n+i) = *(obj.mpi_n+i); } return *this; } int main() { : : //省略 : //List7と同様 : } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] コンストラクタ コンストラクタ コピーコンストラクタ デストラクタ 5 4 デストラクタ デストラクタ └───────────────────────────────────┘ ●その他の演算子オーバーロード ◆[]と()演算子 ・[]は配列添え字の演算子、()は仮引数の設定になりますが、これらは演算子の性質 上メンバ演算子関数としてのみ定義できます。このタイプの演算子関数は 型 クラス名::operator[](int 整数){ } →A[3]等に対応 型 クラス名::operator()(引数リスト){ } →A(3, 2.0)等に対応 と表記されます。 ・これらの演算子については、このような形式で表記できることを認識しているだけで 十分だと思います。自分で使うというよりは、STL等で多用されている概念を理解す るための知識と捉えて下さい。 [Revision Table] |Revision |Date |Comments |----------|-----------|----------------------------------------------------- |1.00 |2002-07-14 |初版 |1.01 |2002-07-22 |リンク追加 [end] |
Copyright(C) 2002 Altmo
|
[C++ Index Top] [Prev] [Next] |