オーバーロード(多重定義)

5章で関数は,引数の型や引数の個数が異なればオーバーロードすることができることを学んだ.ここでは,クラスのコンストラクタと演算子のオーバーロードについて学ぶ.

オペレータ(演算子)のオーバーロード(Overloading Operators)

C++には45のオペレータが用意されている.いくつかあげると

::     スコープ .     のメンバ <<     ビットシフト =     代入 &&    論理積 ||     論理和

などがある.これらのオペレータをオーバーロードする方法を学ぶ

説明を分かりやすくするために,ここでは分数クラスを例にとってコンストラクタとオペレータのオーバーロードを行なう.そこで,まずは分数クラスの定義が必要となる.

例題 11..1   分数クラス
分数の演算を行えるRatioクラスを作成せよ.

解答

  1. 分数の計算を行うプログラムを作成するので,Ratioクラスを宣言する.
  2. 分数は分子と分母で構成されているので,分子と分母を引数とするコンストラクタRatio(int n, int d)を作成する.
  3. コンストラクタの引数はメンバ変数へ代入されるので,メンバ変数として分子と分母を宣言する.
  4. 分数を表示するメンバ関数を作成する.

Ratio
num
den
display()

#include <iostream>
using namespace std;
class Ratio
{
    private:
        int num, den;
    public:
        Ratio(int n, int d);
        void display();
};
Ratio::Ratio(int n, int d)
{
    num = n;
    den = d;
}
void Ratio::display()
{
    cout << num << "/" << den;
}
int main()
{
    Ratio x(22,7);
    cout << "x = ";
    x.display();
}

実行結果

\begin{figure}% latex2html id marker 3431
\centering
\includegraphics[width=8.0cm]{CPPPIC/reidai11-1.eps}
\end{figure}

クラスのコンストラクタは,作成するオブジェクト用に格納場所を用意し,初期化する.また,

Ratio (int n, int d);
Ratio::Ratio(int n, int d)
{
    num = n;
    den = d;
}
と書く代わりに
Ratio (int n, int d) {num = n, den = d}
または,初期化リストを用いて
Ratio (int n, int d) : num(n), den(d) { }
と書くこともできることを学んだ.

コンストラクタのオーバーロード

クラスのコンストラクタ関数は,コピーコンストラクタを作成するときにオーバーロードされた.他にも,柔軟性を得るためや,配列をサポートするためにコンストラクタ関数のオーバーロードは用いられる.例えば,

Ratio() : num(0), den(1) { }
Ratio(int n) : num(n), den(1) { }
Ratio(int n, int d) : num(n), den(d) { }
と3つのコンストラクタを用意する代わりに
Ratio(int n=0, int d= 1) : num(n), den(d) { }
と1つで表すこともできる.

練習問題 11..1   分数クラスRatioを1つのコンストラクタで書き直し,Ratio x(22,7)とRation y(3)を求めよ.

代入オペレータのオーバーロード(Overloading The Assignment Operator)

代入オペレータ = は,オペレータの中で最もよく用いられるオペレータである.その目的は,オブジェクトを別のオブジェクトにコピーすることである.ディフォルトのコンストラクタのように,代入オペレータは自動的に作成されるが,明示的に作成することもできる.

Ratioクラスに代入オペレータを加える

#include <iostream>
using namespace std;
class Ratio
{
    private:
        int num, den;
    public:
        Ratio(int n=0, int d=1):num(n), den(d) {}
        Ratio(const Ratio&); // コピーコンストラクタ
        void operator=(const Ratio&); // 代入オペレータ
};

operator=()
で表される.その引数は,コピーコンストラクタと同じである.

次に,多重定義された代入オペレータの実装部は

void Ratio::operator=(const Ratio& r)
{
    num = r.num;
    den = r.den;
}
で,分数クラスオブジェクトrのメンバ変数を,これを呼んだオブジェクトにコピーする.

This ポインタ(this Pointer)

C++には,thisという名前の特別なポインタがある.thisは全てのメンバ変数の呼び出し時に自動的に渡されるポインタで,その呼び出しを行なったオブジェクトを指す.例えば,
obj.f();
と書くと,f()関数にはobjのポインタが自動的に渡される.ここで,objは関数を呼び出したオブジェクト.このポインタはthisという名前で参照できる.

thisポインタが渡されるのは,メンバ関数だけである.

\begin{figure}\centering
\includegraphics[width=6.0cm]{CPPPIC/fig11-1.eps}
\end{figure}

C++では次のように代入を書くことができる.

x = y = z = 3.14;
このとき,最初にzに3.14が代入され,そして,yに,最後にxとなっている.しかし,代入オペレータはoperator=()という関数である.よって,zに3.14が代入され,次にyに3.14が代入されるということは,最初に代入されたのと同じ値が戻り値として用いられなければならない.よって,代入オペレータは代入されたオブジェクトと同じ型の参照が戻されるべきである.したがって,代入オペレータは
Ratio& operator=(const Ratio& );
と表される.

例題 11..2   オーバーロード
代入オペレータのオーバーロードを行ないRatio型オブジェクトx(22,7)をRatio型オブジェクトzへ代入せよ.

解答 代入オペレータがクラスメンバ関数としてオーバーロードされたとき,オブジェクトと同じ型の参照が戻されるが,これには名前がない.そこで,これをthisとよぶ.これより,正しい代入オペレータのオーバーロード法は次のようになる.

#include <iostream>
using namespace std;
class Ratio
{
    public:
        Ratio(int n=0, int d=1) : num(n),den(d) {}
        Ratio(const Ratio& r) : num(r.num), den(r.den) {}
        Ratio& operator=(const Ratio&);
        void display();
    private:
        int num, den;
};
Ratio& Ratio::operator=(const Ratio& r)
{
    num = r.num;
    den = r.den;
    return *this;
}
void Ratio::display()
{
    cout << num << "/" << den;
}
int main()
{
    Ratio x(22,7);
    Ratio y(x);
    Ratio z;
    cout << "x = " ;
    x.display();
    cout << "\ny = ";
    y.display();
    z = x;
    cout << "\nz = ";
    z.display();
}

実行結果

\begin{figure}% latex2html id marker 3460
\centering
\includegraphics[width=8.0cm]{CPPPIC/reidai11-2.eps}
\end{figure}

これにより,Ratioクラスにコピーコンストラクタと代入オペレータをオーバーロードすることができた. 代入は初期化と同じ記号を用いるが意味は異なる.

Ratio x(22,7); // これは初期化
Ratio y(x); // これは初期化
Ratio z = x; // これは初期化
Ratio w;  
w = x; // これは代入

初期化はコピーコンストラクタをよび,代入は代入オペレータをよぶ.

これより,クラスTにおける代入オペレータの正しい多重定義は
T& T::operaot=(const T& t)
 {
   // オブジェクトtのメンバ変数にオーナーのメンバ変数を代入する
return *this;
 }
となる.

算術オペレータのオーバーロード(Overloading Arithmetic Operator)

すべてのプログラミング言語は標準の四則演算のオペレータ+, - , *, / を用意している.よって,これらをRatioクラスでも定義するのは自然なことである.古いコンピュータ言語,例えばCなどでは,次のような関数を定義することでこれを行ってきた.

Ratio product(Ratio x, Ratio y)
{
nbsp;   Ratio z(x.num*y.num, x.den*y.den);
nbsp;   return z;
}

これでもうまくいくが,関数は次のようにしてよばなくてはいけない.
z = product(x,y);

C++ではこのような関数を,標準の算術オペレータ記号を用いて定義することができる.つまり,
z = x*y;
と定義することができる.乗算オペレータ*は他のC++のオペレータと同様,予約語operatorを用いてoperator*と表す.これをproductの変わりに用いると

Ratio operator*(Ratio x, Ratio y)
{
nbsp;   Ratio z(x.num*y.num, x.den*y.den);
nbsp;   return z;
}
となる.しかし,これはメンバ関数ではない.C++の算術演算子は1つの引数をとる非静的メンバ関数か,2つの引数をとる非メンバ関数によって定義される.つまり,2項演算子*について,aa*bbは,aa.operator*(bb)または,operator*(aa,bb)と解釈される.両方が定義されていれば,多重定義の解決規則によりどちらの解釈を用いるかが決まる.

フレンド関数(friend function)

フレンド関数
として宣言する方法がある.フレンド関数はメンバ関数ではないが,クラスのすべてのメンバにアクセスすることができる関数である.

フレンド関数は,メンバではない通常の関数として定義する.ただし,フレンドとするクラス宣言の内部に,キーワードfriendを先頭につけて関数のプロトタイプを含める.

例題 11..3   フレンド関数
Ratioクラスにフレンド関数を用いてoperator*を定義し, $\frac{22}{7}$ $\frac{-3}{8}$の積を求めよ.

解答

#include <iostream>
using namespace std;
class Ratio
{
    friend Ratio operator*(const Ratio&, const Ratio&);
    public:
        Ratio(int n=0, int d=1) : num(n),den(d) {}
        Ratio(const Ratio& r) : num(r.num), den(r.den) {}
        Ratio& operator=(const Ratio&);
        void display();
    private:
        int num, den;
};
Ratio& Ratio::operator=(const Ratio& r)
{
    num = r.num;
    den = r.den;
    return *this;
}
Ratio operator*(const Ratio& x, const Ratio& y)
{
    Ratio z(x.num*y.num, x.den*y.den);
    return z;
}
void Ratio::display()
{
    cout << num << "/" << den;
}
int main()
{
    Ratio x(22,7);
    Ratio y(-3,8);
    Ratio z;
    z = x;
    z.display();
    cout << endl;
    x = y*z;
    x.display();
    cout << endl;
}

実行結果

\begin{figure}% latex2html id marker 3476
\centering
\includegraphics[width=7.9cm]{CPPPIC/reidai11-3.eps}
\end{figure}

キーワードfriendは関数の実装部には用いられない.また,Ratio::も用いられていない.なぜなら,operator*はメンバ関数ではない. friend関数はメンバ関数ではなく,全てのクラスのメンバからアクセスできる関数である.

練習問題 11..2   friend関数を用いてoperator+をRatioクラスに多重定義し, $\frac{22}{7} + \frac{-3}{8}$を求めよ.

算術代入オペレータのオーバーロード(Overloading the Arithmetic Assignment Operators)

C++では,四則演算と代入オペレータを組み合わせて複合代入演算子を作成することができることを学んだ.例えば,x = x*yと書く代わりに,x *= yと書くことができる.+=,*=などの複合代入演算子は,+や*よりも簡単に定義できることが多い.なぜなら,+処理には3つのオブジェクト(2つの被演算子と計算結果)が関わるのに,+=の処理には2つのオブジェクトしか関わらない.

例題 11..4   複合演算子のオーバーロード
複合演算子*=をRatioクラスにオーバーロードし, $\frac{22}{7} * \frac{-3}{8}$を*=を用いて求めよ.

解答 複合演算子a *= b;はa.operator*=(b);となるので,クラスのメンバとして定義できる.

#include <iostream>
using namespace std;
class Ratio
{
    friend Ratio operator*(const Ratio&, const Ratio&);
    public:
        Ratio(int n=0, int d=1) : num(n), den(d) {}
        Ratio(const Ratio& r) : num(r.num), den(r.den) {}
        Ratio& operator=(const Ratio&);
        Ratio& operator*=(const Ratio&);
        void print();
    private:
        int num, den; //データメンバ
};
Ratio& Ratio::operator=(const Ratio& r)
{
    num = r.num;
    den = r.den;
    return *this;
}
Ratio& Ratio::operator*=(const Ratio& r)
{
    num = num * r.num;
    den = den * r.den;
    return *this;
}
Ratio operator*(const Ratio x, const Ratio y)
{
Ratio z(x.num*y.num, x.den*y.den);
    return z;
}
int main()
{
    Ratio x(22,7), y(-3,8), z;
    z = x;
    z.display();
    cout << endl;
    x = y*z; // 乗算オペレータ
    x.display();
    cout << endl;
    x *= y; // x = x*y
    x.display();
    cout << endl;
}

解答

実行結果

\begin{figure}% latex2html id marker 3503
\centering
\includegraphics[width=7.9cm]{CPPPIC/reidai11-4.eps}
\end{figure}

練習問題 11..3   複合演算子+=をRatioクラスに多重定義し, $\frac{22}{7} + \frac{-3}{8}$を求めよ.

関係演算子のオーバーロード(Overloading the Relation Operators)

6個の関係演算子 $<$, $>$ , $<=$ , $>=$ , == , !=も算術演算子と同じ方法(friend)で,オーバーロードすることができる.

例題 11..5   関係演算子のオーバーロード
Ratioクラスに関係演算子==を多重定義し, $\frac{-3}{8}$ $\frac{9}{-24}$が等しいか調べよ.

#include <iostream>
using namespace std;
class Ratio
{
    friend Ratio operator*(const Ratio&, const Ratio&);
    friend bool operator==(cosnt Ratio&, const Ratio&);
    public:
        Ratio(int n=0, int d=1) : num(n), den(d) {}
        Ratio(const Ratio& r) : num(r.num), den(r.den) {}
        Ratio& operator=(const Ratio&);
        Ratio& operator*=(const Ratio&);
        void print();
    private:
        int num, den; //データメンバ
};
Ratio& Ratio::operator=(const Ratio& r)
{
    num = r.num;
    den = r.den;
    return *this;
}
Ratio& Ratio::operator*=(const Ratio& r)
{
    num = num * r.num;
    den = den * r.den;
    return *this;
}
Ratio operator*(const Ratio x, const Ratio y)
{
    Ratio z(x.num*y.num, x.den*y.den);
    return z;
}
bool operator==(const Ratio x, const Ratio y)
{
    return (x.num * y.num == y.num * x.den);
}
int main()
{
    Ratio x(22,7), y(-3,8), z, w(9,-24);
    z = x;
    z.display();
    cout << endl;
    x = y*z; // 乗算オペレータ
    x.display();
    cout << endl;
    x *= y; // x = x*y
    x.display();
    cout << endl;
    if (y == w) {
        y.display();
        cout << "=";
        w.display();
    }
}

解答

実行結果

\begin{figure}% latex2html id marker 3531
\centering
\includegraphics[width=8.0cm]{CPPPIC/reidai11-5.eps}
\end{figure}

練習問題 11..4   関係演算子!=をRatioクラスに多重定義し, $\frac{22}{6}$ $\frac{55}{15}$が等しいか調べよ.

入出力演算子のオーバーロード(Overloading the Stream Operators)

C++は入力演算子 $>>$ や出力演算子 $<<$ をオーバーロードすることができる. クラスがTでデータメンバがdのとき,出力演算子の書き方は

 friend ostream& operator<<(ostream& ostr, const T& t)
{
    return ostr << t.d;
}
である.ここで,ostreamはiostreamで定義されている標準クラスである.すべてのパラメターと戻り値は参照によって渡される.

例題 11..6   入出力演算子のオーバーロード
Ratioクラスに入出力演算子を多重定義し,分子と分母を別々に入力し,分数を出力せよ.

#include <iostream>
using namespace std;
class Ratio
{
    friend Ratio operator*(const Ratio&, const Ratio&);
    friend bool operator==(cosnt Ratio&, const Ratio&);
    friend ostream& operator<<(ostream&, const Ratio&);
    public:
        Ratio(int n=0, int d=1) : num(n), den(d) {}
        Ratio(const Ratio& r) : num(r.num), den(r.den) {}
        Ratio& operator=(const Ratio&);
        Ratio& operator*=(const Ratio&);
        void print();
    private:
        int num, den; //データメンバ
};
Ratio& Ratio&::operator=(const Ratio& r)
{
    num = r.num;
    den = r.den;
    return *this;
}
Ratio& Ratio&::operator*=(const Ratio& r)
{
    num = num * r.num;
    den = den * r.den;
    return *this;
}
Ratio operator*(const Ratio& x, const Ratio& y)
{
Ratio z(x.num*y.num, x.den*y.den);
    return z;
}
bool operator==(const Ratio& x, const Ratio& y)
{
    return (x.num * y.num == y.num * x.den);
}
ostream& operator<< (ostream& ostr, const Ratio& r)
{
    return ostr << r.num << "/" << r.den;
}
int main()
{
    Ratio x(22,7), y(-3,8), z, w(9,-24);
    z = x; z.display();
    cout << endl;
    x = y*z; // 乗算オペレータ
    x.display();
    cout << endl;
    x *= y; // x = x*y
    x.display();
    cout << endl;
    if (y == w){
        y.display();
        cout << "=";
        w.display();
        cout << endl;
    }
    cout << "x = " << x << ", y = " << y << endl;
}

解答

\begin{figure}% latex2html id marker 3555
\centering
\includegraphics[width=8.0cm]{CPPPIC/reidai11-6.eps}
\end{figure}

入力演算子のオーバーロード(Overloading the Input and Output Operators)

例題 11..7   入力演算子のオーバーロード
入出力演算子をRatioクラスに多重定義し,分子と分母を入力し,分数を出力せよ.

#include <iostream>
using namespace std;
class Ratio
{
    friend istream& operator>>(istream&, Ratio&);
    friend ostream& operator<<(ostream&, const Ratio&);
    public:
        Ratio(int n=0, int d=1) : num(n),den(d) {}
        void swap() {int temp = num; num = den; den = temp;}
        void display();
    private:
        int num, den;
};
ostream& operator<<(ostream& ostr, const Ratio& r)
{
    return ostr << r.num << "/" << r.den;
}
istream& operator>>(istream& istr, Ratio& r)
{
    cout << "\t Numerator: ";
    istr >> r.num;
    cout << "\t Denominator: ";
    istr >> r.den;
    return istr;
}
void Ratio::display()
{
    cout << num << "/" << den;
}
int main()
{
    Ratio x, y;
    cin >> x >> y;
    cout << "x = " << x << ", y = " << y << endl;
}

解答

\begin{figure}% latex2html id marker 3565
\centering
\includegraphics[width=7.2cm]{CPPPIC/reidai11-7.eps}
\end{figure}

アクセス関数(Access Functions)

クラスのメンバ変数はprivate宣言し非公開としているが,クラスにはこのメンバ変数を読むことができるpublicなメンバ関数を用意することが普通である.このようなメンバ関数をアクセス関数(access function)という.

非公開メンバ関数

クラスのメンバ変数は一般に,非公開で,メンバ関数は公開で宣言される.しかし,時にはメンバ関数を非公開で宣言することも必要である.このようなメンバ関数はローカルユーティリティ関数とよばれる. 例えば,分数の計算のときに,分数を既約分数に直しておくほうが扱いやすい.そこで,
class Ratio
{
public:
Ratio(int n=0, int d=1) : num(n), den(d) { reduce();}
void display() {cout $<<$ num $<<$ '/' $<<$ den $<<$ endl;}
private:
int num, den;
void reduce();
};
にように,reduce()関数を宣言する.この関数は,このクラスの中で用いるだけの関数なので,privateとする. さて,既約分数は,分母の分子の最大公約数で割ることにより求まるので,reduce()関数には,最大公約数を求めるgcd()関数が必要である.これより,reduce()関数は,
int gcd(int, int);
void Ratio::reduce()
{
if (num == 0 $\vert\vert$ den == 0){
num = 0;
den = 1;
return;
 }
if (den $<$ 0) {
den *= -1;
num *= -1;
 }
if (den == 1) return;
int sgn = (num < 0 ? -1:1);
int g = gcd(sgn*num, den);
num /= g;
den /= g;
}

練習問題 11..5   int gcd(int, int)を完成させて,入力した分数を分子と分母の最大公約数で割って,分子と分母が互いに素である分数を出力するプログラムを作成せよ.

型変換演算子のオーバーロード(Overloading the Conversion Operators)

型を変換するのに用いられるオペレータを型変換演算子という.もしtypeが変換後の型とすると,型変換演算子は次のように宣言される.
operator type( );
例えば,floatに変換する場合は
operator float( );
となる.また,piなどの定数を変換するときは
operator double( ) const;
となる.

例題 11..8   型変換演算子のオーバーロード
次のプログラムを実行するとどうなるか調べよ.

#include <iostream>
using namespace std;
class Ratio
{
    friend ostream& operator<<(ostream&, const Ratio&);
    public:
        Ratio(int n=0, int d=1) : num(n),den(d) {}
        Operator double() const;
    private:
        int num, den;
};
ostream& operator<<(ostream& ostr, const Ratio& r)
{
    return ostr << r.num << "/" << r.den;
}
Ratio::operator double() const
{
    return double(num)/den;
}
int main()
{
    Ratio x(-5,8);
    cout << "x = " << x << ", double(x) = " << double(x) << endl;
    const Ratio P(22,7);
    const double PI = double(P);
    cout << "P = " << P << ", PI = " << PI << endl;
}

解答

\begin{figure}% latex2html id marker 3592
\centering
\includegraphics[width=7.9cm]{CPPPIC/reidai11-8.eps}
\end{figure}

確認問題 11..1  

1. 予約語operatorはどのように用いられるか.

2. *thisは何を参照しているか

3. メンバ関数以外ではなぜthisポインタを使うことができないのか

4. 次の2つの違いは何か
Ratio y(x);
Ratio y = x;

5. 次の2つの違いは何か
Ratio y = x;
Ratio y; y = x;

6. なぜ,指数演算子**はオーバーロードできないのか.

7. なぜ,出力演算子$<<$と入力演算子$>>$はfriend関数としてオーバーロードされなければならないのか.

演習問題 11..1  

1. Ratioクラスでは,分数の約分も行なえるようにしたい.そこで,次のプログラムを完成せよ.