C++言語解説:2-2.演算子オーバーロード
2002-07-14

[概要] クラスでは演算子を独自に実装することができます。この演算子関数、フレンド
    演算子関数を学習します。代入演算子とコピーコンストラクタの関係についても
    理解します。
    
[構成]・フレンド関数
     * フレンド関数とは
   ・演算子オーバーロード
     * クラスの演算子を定義する
     * メンバ関数による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