静岡理工科大学 菅沼ホーム 全体目次 演習解答例 付録 索引

第11章 演算子のオーバーロード

  1. 11.1 演算子のオーバーロード
    1. (プログラム例 11.1 ) ベクトルの加算(メンバー関数)
    2. (プログラム例 11.2 ) ベクトルの加算(フレンド関数)
  2. 11.2 複素数の加算( + )と出力( << )
    1. (プログラム例 11.3 ) 複素数の加算(メンバー関数)
    2. (プログラム例 11.4 ) 複素数の加算(フレンド関数)
  3. 11.3 変換演算子(Javaを除く)
    1. (プログラム例 11.5 ) 変換演算子
  4. 11.4 関数呼び出し () の多重定義(Javaを除く)
    1. (プログラム例 11.6 ) 関数オブジェクト
    2. (プログラム例 11.7 ) 関数オブジェクトを使用したニュートン法
    3. (プログラム例 11.8 ) イテレータ
  5. 11.5 演算子のオーバーロードと演算子との関係
  6. 演習問題11

11.1 演算子のオーバーロード 

  クラスのオブジェクトどうしの演算はできないでしょうか.例えば,2 次元ベクトルに対応する次のようなクラスを宣言したとします.
	class Vector {
		double x[2];
	}
		
このとき,
	Vector a,b,c;
	c = a + b;
		
のようなことが可能であれば,ベクトルどうしの演算を普通のスカラーと同じように書くことができ,非常に便利です.

  C++ には,上のことを可能にするため,演算子のオーバーロード多重定義operator overloading)という機能があります.演算子のオーバーロードは,以下の 4 つの演算子,
	".",".*","::","?*"
		
を除くすべての演算子に対して可能です.例えば,上の場合のように,+ 演算子に対してオーバーロードを行いたければ,希望する + 演算を実行する関数
	operator+
		
を定義してやればよいわけです.コンパイラは,+ 演算子に出会うと,それが結びつけている各項の型を判断し,基本データ型であれば普通の演算を,また,オーバーロードの際宣言されたクラスのオブジェクトであれば,オーバーロードの定義に従った演算を行います.

  演算子のオーバーロードを行うときは,以下のような規則に従う必要があります.

  以下の 2 つのプログラム例は,ベクトルの加算演算をメンバー関数,及び,フレンド関数を使用して書いた場合の例です.2 つの方法の違いを理解して下さい.

(プログラム例 11.1 ) ベクトルの加算(メンバー関数) 

  このプログラムでは,メンバー関数を使用して,2 次元のベクトルの和(要素どうしの和)を行う + 演算を定義しています.この関数の引数が 1 つしかない理由は以下に示す通りです.例えば,
	c = a + b;
		
を行う場合,operator+ は Vector クラスのメンバー関数ですので,上の演算は,
	c = a.(operator+)(b);
		
と解釈され,下の例のように,関数内で最初のオブジェクト a のメンバーを直接参照できるからです.

/**********************************/
/* ベクトルの加算(メンバー関数) */
/*      coded by Y.Suganuma       */
/**********************************/
#include <stdio.h>

/**********************/
/* クラスvectorの定義 */
/**********************/
class Vector {
		double x[2];
	public:
		Vector operator+ (Vector &b);   // 演算子+のオーバーロード
		void input();
		void output();
};

/**********************/
/* +のオーバーロード */
/**********************/
Vector Vector::operator+ (Vector &b)
{
	Vector c;
	c.x[0] = x[0] + b.x[0];
	c.x[1] = x[1] + b.x[1];
	return c;
}

/**********************/
/* ベクトル要素の入力 */
/**********************/
void Vector::input()
{
	printf("   2つの値を入力してください ");
	scanf("%lf %lf", &(x[0]), &(x[1]));
}
/**********************/
/* ベクトル要素の出力 */
/**********************/
void Vector::output()
{
	printf("%f %f\n", x[0], x[1]);
}

/************/
/* main関数 */
/************/
int main()
{
	Vector a, b, c;

	printf("a\n");
	a.input();
	printf("b\n");
	b.input();

	c = a + b;

	c.output();

	return 0;
}
		
  このプログラムを実行すると,以下に示すような結果が得られます.
a
   2つの値を入力してください 1 2
b
   2つの値を入力してください 3 5
4.000000 7.000000
		

(プログラム例 11.2 ) ベクトルの加算(フレンド関数)

  上の例と同じ内容を,フレンド関数を使用して実現しています.引数の違いに注意して下さい.

/**********************************/
/* ベクトルの加算(フレンド関数) */
/*      coded by Y.Suganuma       */
/**********************************/
#include <stdio.h>

/**********************/
/* クラスvectorの定義 */
/**********************/
class Vector {
		double x[2];
	public:
		void input();
		void output();
	friend Vector operator+ (Vector &a, Vector &b);
};

/**********************/
/* +のオーバーロード */
/**********************/
Vector operator+ (Vector &a, Vector &b)
{
	Vector c;
	c.x[0] = a.x[0] + b.x[0];
	c.x[1] = a.x[1] + b.x[1];
	return c;
}

/**********************/
/* ベクトル要素の入力 */
/**********************/
void Vector::input()
{
	printf("   2つの値を入力してください ");
	scanf("%lf %lf", &(x[0]), &(x[1]));
}
/**********************/
/* ベクトル要素の出力 */
/**********************/
void Vector::output()
{
	printf("%f %f\n", x[0], x[1]);
}

/************/
/* main関数 */
/************/
int main()
{
	Vector a, b, c;

	printf("a\n");
	a.input();
	printf("b\n");
	b.input();

	c = a + b;

	c.output();

	return 0;
}
		

11.2 複素数の加算( + )と出力( << )

  次の 2 つの例も,メンバー関数とフレンド関数との違いを示したものです.これらの例は,複素数型データを扱うためのクラスを定義しており,複素数に対する加算のオーバーロードを定義しています.また,通常の変数と同じように出力ができるようにするため,<< のオーバーロードも参照型関数として定義しています.

  まず,プログラム例 11.3 では,メンバー関数を使用して処理しています.少なくとも,プログラム例のような複素数どうしの演算は正しく行われます.また,
	z = x + 5;
		
のように,複素数に整数を加える演算も,コンストラクタが整数を複素数に変換してくれることにより正しく実行されます.

  しかし,
	z = 5 + x;
		
の場合は,演算子 + に対する最初の引数が基本データ型( int )であるため,通常の加算とみなされ,コンパイラからエラーメッセージが出力されます.このことは,先に述べたように,メンバー関数による + 演算子のオーバーロードが,a.(operator+)(b) と解釈される,つまり,a が複素数型のオブジェクトでなければならないことから明らかです.

  上で述べた 2 種類の演算を正しく実行するためには,次のプログラム例 11.4 のように,演算子 + に対して,各引数を対称に扱うフレンド関数を使用する必要があります.

(プログラム例 11.3 ) 複素数の加算(メンバー関数) 

/****************************/
/* 複素数(メンバー関数)   */
/*      coded by Y.Suganuma */
/****************************/
#include <iostream>

using namespace std;

class Complex {
		double r;
		double i;
	public:
		Complex (double a = 0.0, double b = 0.0);         // constructor
		Complex operator +(Complex a);                 // overload of +
	friend ostream& operator << (ostream&, Complex);   // overload of <<
};

Complex::Complex(double a, double b)
{
	r = a;
	i = b;
}

Complex Complex::operator +(Complex a)
{
	Complex b;
	b.r = r + a.r;
	b.i = i + a.i;
	return b;
}

ostream& operator << (ostream& stream, Complex a)
{
	stream << "(" << a.r << ", " << a.i << ")\n";
	return stream;
}

int main()
{
	Complex w, x(1.0, 2.0), y(3.0), z;
	z = x + y;
	w = x + 5.0;   // 5.0 + x はエラー
	cout << "z " << z ;
	cout << "w " << w ;
	return 0;
}
		
  このプログラムを実行すると,以下に示すような結果が得られます.
z (4, 2)
w (6, 2)
		

(プログラム例 11.4 ) 複素数の加算(フレンド関数)

/****************************/
/* 複素数(フレンド関数)   */
/*      coded by Y.Suganuma */
/****************************/
#include <iostream>

using namespace std;

class Complex {
		double r;
		double i;
	public:
		Complex (double a = 0.0, double b = 0.0);         // constructor
	friend Complex operator +(Complex a, Complex b);   // overload of +
	friend ostream& operator << (ostream&, Complex);   // overload of <<
};

Complex::Complex(double a, double b)
{
	r = a;
	i = b;
}

Complex operator +(Complex a, Complex b)
{
	Complex c;
	c.r = a.r + b.r;
	c.i = a.i + b.i;
	return c;
}

ostream& operator << (ostream& stream, Complex a)
{
	stream << "(" << a.r << ", " << a.i << ")\n";
	return stream;
}

int main()
{
	Complex w, x(1.0, 2.0), y(3.0), z;
	z = x + y;
	w = 5.0 + x;
	cout << "z " << z ;
	cout << "w " << w ;
	return 0;
}
		
  このプログラムを実行すると,以下に示すような結果が得られます.
z (4, 2)
w (6, 2)
		

11.3 変換演算子(Javaを除く)

  キーワード operator を使用して,オブジェクトを別のデータ型へ変換するメンバー関数を定義できます.これを変換演算子と呼びます.変換演算子の用途として,オブジェクトから基本データ型への変換があります.これは,オブジェクトの状態を調べる等のために使用されます.例えば,クラス vector の例に応用するとすれば,0 でない要素を含むベクトルの場合に 1,その他の場合は 0 となる整数に変換する例を下に示します.

(プログラム例 11.5 ) 変換演算子

/****************************/
/* 変換演算子               */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

/**********************/
/* クラスVectorの定義 */
/**********************/
class Vector {
		double x[2];
	public:
		operator int()     // 変換演算子の定義
		{
			return (x[0] != 0.0 || x[1] != 0.0);
		}
	friend void input(Vector &);
	friend void output(Vector &);
};
/**********************/
/* ベクトル要素の入力 */
/**********************/
void input(Vector &v)
{
	scanf("%lf %lf", &(v.x[0]), &(v.x[1]));
}
/**********************/
/* ベクトル要素の出力 */
/**********************/
void output(Vector &v)
{
	printf("%f %f\n", v.x[0], v.x[1]);
}
/************/
/* main関数 */
/************/
int main()
{
	Vector a;
	int sw = 0;

	while (sw == 0) {
		printf("要素を入力してください ");
		input(a);
		sw = int(a);    // オブジェクトaの型をintに変換
	}

	output(a);

	return 0;
}
		
  このプログラムを実行すると,以下に示すような結果が得られます.
要素を入力してください 0 0
要素を入力してください 1 0
1.000000 0.000000
		

11.4 関数呼び出し () の多重定義(Javaを除く)

  関数呼び出し () の多重定義によって,以下に示すように,オブジェクトを関数のように使用することが可能です.このようなオブジェクトを関数の引数として利用することにより,関数のアドレスを引き渡す場合と同様のことが可能になります(プログラム例 11.7 参照).

(プログラム例 11.6 ) 関数オブジェクト

#include <stdio.h>
					// () のオーバーロード
class Plus
{
	public :
		int operator() (int a, int b)
		{
			return a + b;
		}
};
					// main
int main()
{
	Plus p;
	int c = p(10, 20);
	printf("c = %d\n", c);
	return 0;
}
		
  このプログラムを実行すると,以下に示すような結果が得られます.
c = 30
		

(プログラム例 11.7 ) 関数オブジェクトを使用したニュートン法

  次のプログラムの関数 newton は,ニュートン法により非線形方程式 f(x) = 0 (この例では,ex - 3x = 0 )の解を求めるためのものです.内容はプログラム例 7.14 と同じですが,関数名の代わりに,f(x) 及び f(x) の微分を計算する関数オブジェクト( snx_f と dsnx_f )を引数としています.ただし,ニュートン法とは,関数 f(x) が単調連続で変曲点が無く,かつ,微分可能であるとき利用できる方法であり,根の適当な初期値 x0 から始めて,反復公式
	xn+1 = xn - f(xn) / f'(xn)
		
を繰り返すことによって,非線形方程式の解を求める方法です.

/******************************************/
/* 関数オブジェクトを使用したニュートン法 */
/*          coded by Y.Suganuma           */
/******************************************/
#include <stdio.h>
#include <math.h>

class snx   // 関数値(f(x))の計算
{
	public:
		double operator() (double x)
		{
			return exp(x) - 3.0 * x;
		}
};

class dsnx   // 関数の微分の計算
{
	public:
		double operator() (double x)
		{
			return exp(x) - 3.0;
		}
};

double newton(snx, dsnx, double, double, double, int, int *);

int main()
{
	double eps1, eps2, x, x0;
	int max, ind;
	snx snx_f;
	dsnx dsnx_f;

	eps1 = 1.0e-7;
	eps2 = 1.0e-10;
	max  = 20;
	x0   = 0.0;

	x = newton(snx_f, dsnx_f, x0, eps1, eps2, max, &ind);

	printf("ind=%d  x=%f  f=%f  df=%f\n",ind,x,snx_f(x),dsnx_f(x));

	return 0;
}

/*****************************************************/
/* Newton法による非線形方程式(f(x)=0)の解            */
/*      fn : f(x)を計算する関数オブジェクト          */
/*      dfn : f(x)の微分を計算する関数オブジェクト   */
/*      x0 : 初期値                                  */
/*      eps1 : 終了条件1(|x(k+1)-x(k)|<eps1)   */
/*      eps2 : 終了条件2(|f(x(k))|<eps2)       */
/*      max : 最大試行回数                           */
/*      ind : 実際の試行回数                         */
/*            (負の時は解を得ることができなかった) */
/*      return : 解                                  */
/*****************************************************/
double newton(snx fn, dsnx dfn, double x0, double eps1, double eps2, int max, int *ind)
{
	double g, dg, x, x1;
	int sw;

	x1   = x0;
	x    = x1;
	*ind = 0;
	sw   = 0;

	while (sw == 0 && *ind >= 0) {

		sw    = 1;
		*ind += 1;
		g     = fn(x1);

		if (fabs(g) > eps2) {
			if (*ind <= max) {
				dg = dfn(x1);
				if (fabs(dg) > eps2) {
					x = x1 - g / dg;
					if (fabs(x-x1) > eps1 && fabs(x-x1) > eps1*fabs(x)) {
						x1 = x;
						sw = 0;
					}
				}
				else
					*ind = -1;
			}
			else
				*ind = -1;
		}
	}

	return x;
}
		
  このプログラムを実行すると,以下に示すような結果が得られます.
ind=5  x=0.619061  f=0.000000  df=-1.142816
		

(プログラム例 11.8 ) イテレータ

  単純な配列であれば,添え字やポインタを使用して最初の要素,最後の要素,次の要素などを簡単に参照することができます.しかし,リスト構造などに対しては,それほど簡単ではありません.次の例では,「次の要素」を参照するイテレータを関数呼び出し () の多重定義によって作成しています(この例では,単純な配列ですが).

#include <stdio.h>
					// 構造体 list
struct list
{
	char *name;
	int nen;
};
					// クラス Vector
class Vector
{
		list *val;
		int max;
	public:
							// コンストラクタ
		Vector(int n)
		{
			val = new list [n];
			max = n;
		}
							// デストラクタ
		~Vector()
		{
			delete [] val;
		}
	friend class IteratorNext;
};
					// クラス IteratorNext
class IteratorNext
{
		const Vector *vp;
		int i;
	public:
							// コンストラクタ
		IteratorNext (const Vector &v)
		{
			vp = &v;
			i  = 0;
		}
							// 次の要素
		list *operator() ()
		{
			return (i < vp->max) ? &(vp->val[i++]) : 0;
		}
};
					// main
int main()
{
	Vector v(2);
	list *p;
	IteratorNext next1(v);
	p       = next1();
	p->name = "山田太郎";
	p->nen  = 30;
	p       = next1();
	p->name = "鈴木花子";
	p->nen  = 25;
	IteratorNext next2(v);
	while ((p = next2()) != 0)
		printf("%s %d\n", p->name, p->nen);
	return 0;
}
		

  このプログラムを実行すると,次のような出力が得られます.
	山田太郎 30
	鈴木花子 25
		
11.5 演算子のオーバーロードと演算子との関係

  前節までは,主として + 演算子を例としてオーバーロードの説明をしましたが,この節では,各演算子とそのオーバーロードを実行する関数 operator・ ( 「 ・ 」 を演算子とする)との関係について詳細に説明します.

  1. 二項演算子  フレンド関数の場合は,
    	a = b ・ c  →  a = operator・(b, c)
    			
    また,メンバー関数の場合は,
    	a = b ・ c  →  a = b.operator・(c)
    			
    の関係があります.前節の + 演算子の例を参考にして下さい.

  2. 単項演算子  フレンド関数の場合は,
    	a = ・b  →  a = operator・(b)
    			
    また,メンバー関数の場合は,
    	a = ・b  →  a = b.operator・()
    			
    の関係があります.

  3. 添え字([])演算子  この演算子に対しては,メンバー関数だけで定義できます.引数は 1 つしかとれませんが,int 以外の型でも構いません.
    	a = b[c]  →  a = b.operator[](c)
    			

  4. 関数呼び出し(())演算子  この演算子に対しては,メンバー関数だけで定義できます.
    	a = b(c,d,・・・)  →  a = b.operator()(c,d,・・・)
    			

演習問題11

[問1]例えば,
のように,加減乗除に対する分数演算を実行するためのクラスを定義せよ.ただし,約分は考慮しなくても良い.

[問2]時刻の加算及び減算を行うクラスを定義せよ.

[問3]日付(紀元後とし,1990年6月23日なら 1990/6/30 と表現する)の加算及び減算を行うクラスを定義せよ.ただし,
加算:1990/6/30 + 45 → 1990年6月23日から 45 日後の年月日を計算
   1990/6/30 + (-23) → 1990年6月23日から 23 日前の年月日を計算
減算:1990/6/30 - 1985/12/1 → 1985年12月1日は 1990年6月23日みて何日前かを計算
   1990/6/30 - 1995/12/1 → 1995年12月1日は 1990年6月23日みて何日後かを計算
また,閏年は,年号が 4 で割り切れ,かつ,100 で割り切れない年か,または,400 で割り切れる年である.

[問4]行列の加減乗除を実行するためのクラスを定義せよ.ただし,ここで割り算 A/B とは,B の逆行列を左から掛けること,つまり,B-1A を意味するものとする.

静岡理工科大学 菅沼ホーム 全体目次 演習解答例 付録 索引