複素数クラスの例
隠蔽化と基本的なオブジェクト指向の練習課題として、前回の授業では、直交座標系による複素数クラスを示した。今回の授業では、演習を行うとともに直交座標系を極座標系にクラス内部を変更したことにより、隠蔽化の効果について考えてもらい、第1回レポートとする。
直交座標系
前回の授業で示した直交座標系のクラス。比較対象とするために再掲。
#include <stdio.h>
#include <math.h>
// 直交座標系の複素数クラス
class Complex {
private:
double re ; // 実部
double im ; // 虚部
public:
void print() {
printf( "%lf + j%lf¥n" , re , im ) ;
}
Complex( double r , double i ) // 実部虚部のコンストラクタ
: re( r ) , im( i ) {}
Complex() // デフォルトコンストラクタ
: re( 0.0 ) , im( 0.0 ) {}
void add( Complex z ) {
// 加算は、直交座標系だと極めてシンプル
re = re + z.re ;
im = im + z.im ;
}
void mul( Complex z ) {
// 乗算は、直交座標系だと、ちょっと煩雑
double r = re * z.re - im * z.im ;
double i = re * z.im + im * z.re ;
re = r ;
im = i ;
}
double get_re() {
return re ;
}
double get_im() {
return im ;
}
double get_abs() { // 絶対値
return sqrt( re*re + im*im ) ;
}
double get_arg() { // 偏角
return atan2( im , re ) ;
}
} ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね
int main() {
// 複素数を作る
Complex a( 1.0 , 2.0 ) ;
Complex b( 2.0 , 3.0 ) ;
// 複素数の計算
a.print() ;
a.add( b ) ;
a.print() ;
a.mul( b ) ;
a.print() ;
return 0 ;
}
極座標系
上記の直交座標系の Complex クラスは、加減算の関数は単純だけど、乗除算の関数を書く時には面倒になってくる。この場合、極座標系でプログラムを書いたほうが判りやすいかもしれない。
// 局座標系の複素数クラス
class Complex {
private:
double r ; // 絶対値 r
double th ; // 偏角 θ
public:
void print() {
printf( "%lf ∠ %lf¥n" , r , th / 3.14159265 * 180.0 ) ;
}
Complex() // デフォルトコンストラクタ
: r( 0.0 ) , th( 0.0 ) {}
// 表面的には、同じ使い方ができるように
// 直交座標系でのコンストラクタ
Complex( double x , double y ) {
r = sqrt( x*x + y*y ) ;
th = atan2( y , x ) ; // 象限を考慮したatan()
}
// 極座標系だと、わかりやすい処理
void mul( Complex z ) {
// 極座標系での乗算は
r = r * z.r ; // 絶対値の積
th = th + z.th ; // 偏角の和
}
// 反対に、加算は面倒な処理になってしまう。
void add( Complex z ) {
; // 自分で考えて
}
// ゲッターメソッド
double get_abs() {
return r ;
}
double get_arg() {
return th ;
}
double get_re() { // 直交座標系との互換性のためのゲッターメソッド
return r * cos( th ) ;
}
double get_im() {
return r * sin( th ) ;
}
} ; // ←しつこく繰り返すけど、セミコロン忘れないでね(^_^;
このように、プログラムを開発していると、当初は直交座標系でプログラムを記述していたが、途中で極座標系の方がプログラムが書きやすいという局面となるかもしれない。しかし、オブジェクト指向による隠蔽化を正しく行っていれば、利用者に影響なく「データ構造」や「その手続き(メソッド)」を書換えることも可能となる。
このように、プログラムをさらに良いものとなるべく書換えることは、オブジェクト指向ではリファクタリングと呼ぶ。
正しくクラスを作っていれば、クラス利用者への影響が最小にしながらリファクタリングが可能となる。
const 指定 (経験者向け解説)
C++ では、間違って値を書き換えるような処理を書けないようにするための、const 指定の機能がある。
void bar( char* s ) { // void bar( const char* s ) {...}
printf( "%s\n" , s ) ; // で宣言すべき。
}
void foo( const int x ) {
// ~~~~~~~~~~~
x++ ; // 定数を書き換えることはできない。
printf( "%d\n" , x ) ;
}
int main() {
const double pi = 3.141592 ;
// C言語で #define PI 3.141592 と同等
bar( "This is a pen" ) ; // Warning: string constant to 'char*' の警告
int a = 123 ;
foo( a ) ;
return 0 ;
}
前述の、getter メソッドの例では要素を参照するだけで、オブジェクトの中身が変化しない。逆に言えば、getter のメソッド内にはオブジェクトに副作用のある処理を書いてはいけない。こういった用途に、オブジェクトを変化させないメソッド宣言がある。先の、get_re() は、
class ... {
:
inline double get_re() const {
// ~~~~~
re = 0 ; // 文法エラー
return re ;
}
} ;
クラスオブジェクトを引数にする場合
前述の add() メソッドでは、”void add( Complex z ) { … }” にて宣言をしていた。しかし、引数となる変数 z の実体が巨大な場合、この書き方では値渡しになるため、データの複製の処理時間が問題となる場合がある。この場合は、(書き方1)のように、z の参照渡しにすることで、データ複製の時間を軽減する。また、この例では、引数 z の中身を間違って add() の中で変化させる処理を書いてしまうかもしれない。そこで、この事例では(書き方2)のように const 指定もすべきである。
// (書き方1)
class Complex {
:
void add( Complex& z ) {
re += z.re ;
im += z.im ;
}
} ;
// (書き方2)
class Complex {
:
void add( const Complex& z )
{ // ~~~~~~~~~~~~~~~~
re += z.re ;
im += z.im ;
}
} ;
レポート1(複素数の加減乗除)
授業中に示した、直交座標系・極座標系の複素数のプログラムをベースに、記載されていない減算・除算のプログラムを作成し、レポートを作成する。 レポートには、下記のものを記載すること。
- プログラムリスト
- プログラムへの説明
- 動作確認の結果
- プログラムより理解できること。
- 実際にプログラムを書いてみて分かった問題点など…