ホーム » 「オブジェクト指向」タグがついた投稿

タグアーカイブ: オブジェクト指向

2018年12月
« 11月    
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

最近の投稿(電子情報)

アーカイブ

カテゴリー

複素数とクラス・隠蔽化の演習

コンストラクタやメソッドの書き方

前回のプログラムを、もう少しC++のオブジェクト指向っぽい書き方を導入してみる。この際に、分かりやすく記述するため、行数が長くなってきた時のための処理を考慮して、記述してみる。

#include <stdio.h>
#include <math.h>
class Complex {
private:
   double re ;
   double im ;
public:
   // 絶対座標系でも極座標系でも、実部・虚部・絶対値・偏角を扱うため
   inline double get_re() const { return re ; }
   inline double get_im() const { return im ; }
   inline double get_r()  const { return sqrt( re*re + im*im ) ; }
   inline double get_th() const { return atan2( im , re ) ; }

   // 極座標系なら、以下のような getterメソッド を書いておけば、
   //   座標系を気にせず処理がかける。
   // inline double get_re() const { return r * cos( th ) ; }
   // inline double get_r()  const { return r ; }

   // inline は、開いたサブルーチンで実装してくれるので処理速度が遅くならない
   // foo() const { ... } は、オブジェクトに副作用が無いことを示す。

   // コンストラクタのメンバー変数の初期化の書き方
   // Complex( ... )
   // : 要素1( 値1 ) , 要素2( 値2 ) ...
   // {  その他の初期化
   // }
   Complex()                       // コンストラクタ
   : re( 0.0 ) , im( 0.0 ) {}
   Complex( double r , double i )  // コンストラクタ
   : re( r ) , im( i ) {}

   // メソッドのプロトタイプ宣言
   //  メソッドの中身が長くなる場合は、
   //  名前と引数だけを宣言しておく
   void print() ;
   void add( Complex ) ;
   void mul( Complex ) ;
} ; // ←しつこいけどセミコロン忘れないでね。

// クラス宣言の外にメソッドの定義を書く
//   メソッドの中身の記述 「::」をクラス限定子と呼ぶ
void Complex::print() {
   printf( "%lf + j%lf¥n" , get_re() , get_im() ) ;
}
void Complex::add( Complex z ) {
   re = re + z.re ;
   im = im + z.im ;
}
void Complex::mul( Complex z ) {
   double r = re * z.re - im * z.im ;
   double i = re * z.im + im * z.re ;
   re = r ;
   im = i ;
}

int main() {
   Complex a( 1.0 , 2.0 ) ;
   Complex b( 2.0 , 4.0 ) ;
   a.add( b ) ;
   a.print() ;
   return 0 ;
}

参考資料

レポート1(複素数の加減乗除)

授業中に示した上記のプログラムをベースに、 記載されていない減算・除算のプログラムを作成し、レポートを作成する。 レポートには、下記のものを記載すること。

  • プログラムリスト
  • プログラムへの説明
  • 動作確認の結果
  • プログラムより理解できること。
    実際にプログラムを書いてみて分かった問題点など…

コンストラクタと複素数クラスと隠蔽化

コンストラクタ

プログラミングでは、データの初期化忘れによる間違いもよく発生する。これを防ぐために、C++ のクラスでは、コンストラクタ(構築子)がある。データ構造の初期化専用の関数。

// コンストラクタ
#include <stdio.h>
#include <string.h>

class Person {
private:
   char name[ 20 ] ;
   int  phone ;
public:
   void print() {
      printf( "%s %d¥n" , name , phone ) ;
   }
   Person() {                             // コンストラクタ(A)
      name[0] = '¥0' ; // 空文字列
      phone = 0 ;
   }
   Person( const char str[] , int tel ) { // コンストラクタ(B)
      strcpy( name , str ) ;
      phone = tel ;
   }
   ~Person() {                            // デストラクタ
      print() ; // 内容の表示
   }
} ;
int main() {
   Person saitoh( "t-saitoh" , 621111 ) ; // (A)で初期化
   Person tomoko() ;                      // (B)で初期化
   saitoh.print() ;  // "t-saitoh 621111" の表示
   tomoko.print() ;  // " 0" の表示

   return 0 ;        // この時点で saitoh.~Person()
                     // tomoko.~Person() が自動的に
}                    // 呼び出される。

コンストラクタと反対に、デストラクタは、データが不要となった時に自動的に呼び出される関数。

複素数クラスの例

隠蔽化と基本的なオブジェクト指向の練習課題として、複素数クラスをあげる。ここでは、複素数の加算・乗算を例に説明をするので、減算・除算などの処理を記述することで、クラスの扱いに慣れてもらう。

直交座標系

#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 = 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 ;
   }
} ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね
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() ;
}

極座標系

上記の直交座標系の Complex クラスは、加減算の関数は単純だけど、乗除算の関数を書く時には面倒になってくる。この場合、極座標系でプログラムを書いたほうが判りやすいかもしれない。

class Complex {
private:
   double r ;  // 絶対値 r
   double th ; // 偏角   θ
public:
   void print() {
      printf( "%lf ∠ %lf¥n" , r , th / 3.14159265 * 180.0 ) ;
   }
   Complex() {
      r = th = 0.0 ;
   }
   Complex( double x , double y ) {
      r  = sqrt( x*x + y*y ) ;
      th = atan2( y , x ) ;    // 象限を考慮したatan()
   }
   void add( Complex z ) {
      ; // 自分で考えて
   }
   void mul( Complex z ) { // 極座標系での乗算は
      r  = r  * z.r  ;     //  絶対値の積
      th = th + z.th ;     //  偏角の和
   }
} ; // ←しつこく繰り返すけど、セミコロン忘れないでね(^_^;

このように、プログラムを開発していると、当初は直交座標系でプログラムを記述していたが、途中で極座標系の方がプログラムが書きやすいという局面となるかもしれない。しかし、オブジェクト指向による隠蔽化を正しく行っていれば、利用者に影響なく「データ構造」や「その手続き(メソッド)」を書換えることも可能となる。

このように、プログラムをさらに良いものとなるべく書換えることは、オブジェクト指向ではリファクタリングと呼ぶ。

引数渡しと構造体からオブジェクト指向へ

値渡し、ポインタ渡し、参照渡し

構造体の使い方の話では、関数との構造体のデータ渡しでポインタなどが出てくるので、 値渡し・ポインタ渡し・参照渡しの復習。(参照渡しはC++で導入された考え方)

値渡し

C言語の基本は、値渡し。呼び出し側の実引数は、関数側の仮引数に値がコピーされる。 このため、呼び出し側の変数(下の例ではa)の中身は変化しない。 よって、関数の呼び出しで呼び出し側の変数が勝手に中身が変わらないので、予想外の変数の中身の変化が無く分かりやすい。

// 値渡し(call by value)の例
void foo( int x ) {
   x++ ;
   printf( "%d¥n" , x ) ;
}
void main() {
   int a = 123 ;
   foo( a ) ;  // 124が表示
   foo( a ) ;  // 124が表示
}

ポインタ渡し

しかし、上の例では、foo()の呼び出しで、変数aの中身が変化してくれたほうが都合が良い場合もある。 この場合は、C言語ではポインタを使って記述する。 このように、関数を呼び出して、手元の変数が変化することは、副作用と呼ばれる。 副作用の多いプログラムは、変数の値の管理がわかりにくくなるので、副作用は最小限に記述すべき。

// ポインタ渡し(call by pointer)の例
void foo( int *px ) {
   (*px)++ ;
   printf( "%d¥n" , (*px) ) ;
}
void main() {
   int a = 123 ;
   foo( &a ) ;  // 124が表示
   foo( &a ) ;  // 125が表示
}

参照渡し

しかし、ポインタを多用すると、ポインタを動かしてトラブルも増えることから、ポインタはあまり使わない方が良い。 そこで、C++では参照型というものがでてきた。

// 参照型(call by reference)の場合
void foo( int &x ) {
   x++ ;
   printf( "%d¥n" , x ) ;
}
void main() {
   int a = 123 ;
   foo( a ) ;  // 124が表示
   foo( a ) ;  // 125が表示
}

参照型は、ポインタを使っていないように見えるけれども、機械語レベルでみればポインタを使ったものと同じ。

構造体でオブジェクト指向もどき

例えば、名前と電話番号の構造体で処理を記述する場合、 以下の様な記載を行うことで、データ設計者データ利用者で分けて 仕事ができることを説明。

// この部分はデータ構造の設計者が書く
// データ構造を記述
struct Person {
   char name[10] ;
   int  phone ;
} ;
// データに対する処理を記述
void readPerson( struct Person* p ) {
   // ポインタの参照で表記
   scanf( "%s%d" ,
          (*p).name , &(*p).phone ) ;
}
void printPerson( struct Person* p ) {
   // アロー演算子で表記
   printf( "%s %d¥n" ,
           p->name , p->phone ) ;
}
// この部分は、データ利用者が書く
int main() {
   // Personの中身を知らなくてもいいから配列を定義(データ隠蔽)
   struct Person table[ 10 ] ;
   for( int i = 0 ; i < 10 ; i++ ) {
      // 入力して、出力する...という雰囲気で書ける(手続き隠蔽)
      readPerson( &table[i] ) ;
      printPerson( &table[i] ) ;
   }
   return 0 ;
}

このプログラムの書き方では、mainの中を読むだけもで、 データ入力とデータ出力を行うことはある程度理解できる。 この時、データ構造の中身を知らなくてもプログラムが理解でき、 データ実装者はプログラムを記述できる。これをデータ構造の隠蔽化という。 一方、readPerson()や、printPerson()という関数の中身についても、 入力・出力の方法をどうするのか知らなくても、 関数名から動作は推測できプログラムも書ける。 これを手続きの隠蔽化という。

C++のクラスで表現

上記のプログラムをそのままC++に書き直すと以下のようになる。

#include <stdio.h>

// この部分はクラス設計者が書く
class Person {
private: // クラス外からアクセスできない部分
   // データ構造を記述
   char name[10] ; // メンバーの宣言
   int  phone ;
public: // クラス外から使える部分
   // データに対する処理を記述
   void read() { // メソッドの宣言
      // pのように対象のオブジェクトを明記する必要はない。
      scanf( "%s%d" , name , &phone ) ;
   }
   void print() {
      printf( "%s %d¥n" , name , phone ) ;
   }
} ; // ← 注意ここのセミコロンを書き忘れないこと。

// この部分はクラス利用者が書く
int main() {
   Person table[ 10 ] ;
   for( int i = 0 ; i < 10 ; i++ ) {
      table[i].read() ;   // メソッドの呼び出し
      table[i].print() ;  // オブジェクト.メソッド()
   }
   // 文法エラーの例
   printf( "%d¥n" , table[0].phone ) ;
   // phoneはprivateなので参照できない。
   return 0 ;
}

用語の解説:C++のプログラムでは、データ構造とデータの処理を、並行しながら記述する。 データ構造に対する処理は、メソッド(method)と呼ばれる。 データ構造とメソッドを同時に記載したものは、クラス(class)と呼ぶ。 そのclassに対し、具体的な値や記憶域が割り当てられたものをオブジェクト(object)と呼ぶ。

オブジェクト指向(2018) / ガイダンス

専攻科2年のオブジェクト指向プログラミングの授業の1回目。最初に授業全般の概要を説明した後、オブジェクト指向の歴史とC言語の構造体の説明。

オブジェクト指向プログラミングの歴史

最初のプログラム言語のFortran(科学技術計算向け)の頃は、処理を記述するだけだったけど、 COBOL(商用計算向け)ができた頃には、データをひとまとめで扱う「構造体」(C言語ならstruct …}の考えができた。 その後のALGOLの頃には、処理をブロック化して扱うスタイル(C言語なら{ 文 … }の複文で 記述する方法ができて、処理の構造化・データの構造化ができる。これが「構造化プログラミング(structured programming)」 の始まりとなる。
この後、様々なプログラム言語が開発され、C言語などもできてきた。 一方で、シミュレーションのプログラム開発(例simula)では、 シミュレーション対象(object)に対して、命令するスタイルの書き方が生まれ、 データに対して命令するという点で、擬人法のようなイメージで直感的にも分かりやすかった。 これがオブジェクト指向の始まりとなる。

この考え方を導入した言語の1つが Smalltalk であり、この環境では、プログラムのエディタも Smalltalk で記述したりして、オブジェクト指向がGUIのプログラムと親和性が良いことから普及が拡大する。

C言語にこのオブジェクト指向を取り入れて、C++が開発される。さらに、この文法をベースとした、 Javaなどが開発される。最近の新しい言語では、どれもオブジェクト指向の考えが使われている。

構造体の導入

C++でのオブジェクト指向は、構造体の表記がベースになっているので、まずは構造体の説明。

// 構造体の宣言
struct Person {      // Personが構造体につけた名前
   char name[ 20 ] ; // 要素1
   int  phone ;      // 要素2
} ;                  // 構造体定義とデータ構造宣言を
                     // 別に書く時は「;」の書き忘れに注意
// 構造体変数の宣言
struct Person saitoh ;
struct Person data[ 10 ] ;
// 実際にデータを参照 構造体変数.要素名
strcpy( saitoh.name , "t-saitoh" ) ;
saitoh.phone = 272925 ;
for( int i = 0 ; i < 10 ; i++ ) {
   scanf( "%d%s" , data[ i ].name , &(data[ i ].phone) ) ;
}