ホーム » スタッフ » 斉藤徹 » 講義録 » オブジェクト指向 (ページ 12)

オブジェクト指向」カテゴリーアーカイブ

2025年5月
 123
45678910
11121314151617
18192021222324
25262728293031

検索・リンク

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

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

構造体の使い方の話では、関数との構造体のデータ渡しでポインタなどが出てくるので、 値渡し・ポインタ渡し・参照渡しの復習。(参照渡しは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) ) ;
}

構造体の参照渡しとオブジェクト指向

一緒に、来週のプログラミング応用の資料書いちゃえ。

構造体の参照渡し

構造体のデータを関数の呼び出しで記述する場合には、参照渡しを利用する。

struct Person {
   char name[ 20 ] ;
   int  age ;
} ;
void print( struct Person* p ) {
   printf( "%s %d¥n" , p->name , p->age ) ;
}
void main() {
   struct Person saitoh ;
   strcpy( saitoh.name , "t-saitoh" ) ;
   saitoh.age = 50 ;
   print( &saitoh ) ;
}

このようなプログラムの書き方をすると、「データ saitoh に、print() せよ…」 といった処理を記述したようになる。 これを発展して、データ saitoh に、print という命令をするイメージにも見える。

この考え方を、そのままプログラムに反映させ、Personというデータは、 名前と年齢、データを表示するprintは…といったように、 データ構造と、そのデータ構造への処理をペアで記述すると分かりやすい。

オブジェクト指向の導入

オブジェクト指向では、データ構造とその命令を合わせたものをクラス(class)と呼ぶ。 また、データ(class)への命令は、メソッド(method)と呼ぶ。

class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( char s[] , int a ) {
      strcpy( name , s ) ;
      age = a ;
   }
   int scan() {
      return scan( "%s %d" , name , &age ) ;
   }
   void print() {
      printf( "%s %d¥n" , name , age ) ;
   }
} ;
void main() {
   Person saitoh( "t-saitoh" , 50 ) ;
   saitoh.print() ;
   Person table[ 50 ] ;
   for( int i = 0 ; i < 50 ; i++ ) {
      if ( table[ i ].scan() != 2 )
         break ;
      table[ i ].print() ;
   }
}

構造体とオブジェクト指向

プログラミング応用の後期では、構造体とコンピュータグラフィックスの基礎を扱う予定。 CGの基礎でも、X座標,Y座標…をひと塊の構造体で表現という意味では、構造体の延長として授業を進める予定。

構造体

上記資料を元に説明。 最初に構造体が無かったら、名前・国語・算数・理科の1クラス分のデータをどう表現しますか?

// まずは基本の宣言
char name[ 50 ][ 20 ] ;
int  kokugo[ 50 ] ;
int  sansu[ 50 ] ;
int  rika[ 50 ] ;
// もしクラスが最初20人だったら、20→50に変更する際に、
// 文字列長の20も書きなおしちゃうかも。
// 50とか20とかマジックナンバーは使わないほうがいい。
#define SIZE 50
#define LEN 20
char name[ SIZE ][ LEN ] ;
int  kokugo[ SIZE ] ;
:
// 2クラス分のデータ(例えばEI科とE科)を保存したかったら?
// case-1(配列2倍にしちゃえ)
char name[ 100 ][ 20 ] ;  // どこからがEI科?
int  kokugo[ 100 ] ;
:
// case-2(2次元配列にしちゃえ)
char name[ 2 ][ 50 ][ 20 ] ; // 0,1どっちがEI科?
int  kokugo[ 2 ][ 50 ] ;
:
// case-3(目的に応じた名前の変数を作っちゃえ)
char ei_name[ 50 ][ 20 ] ; // EI科は一目瞭然
int  ei_kokugo[ 50 ] ;     // だけど変数名が違うから
:                      // 処理を2度書き
char ee_name[ 50 ][ 20 ] ;
int  ee_kokugo[ 50 ] ;
:

このような問題に対応するために構造体を用いる。

struct Person {  // Personが構造体名(タグ名)
   char name[ 20 ] ;
   int  kokugo ;
   int  sansu ;
   int  rika ;
} ;
struct Person saitoh ;
struct Person ei[ 50 ] , ee[ 40 ] ;
strcpy( saitoh.name , "t-saitoh" ) ;
saitoh.kokugo = 100 ;
ei[ 0 ].sansu = 80 ;
ee[ 1 ].rika = 75 ;

授業では、構造体の初期化、入れ子の話をする。詳細は配布資料参照。

途中で、C言語の歴史として、unix開発時に、BCPL→B言語→C言語(K&R)→ANSI-C→…C++→D言語 といった雑談も説明。

入れ子の話では、 for(…) { for(…) { } } のような、処理の入れ子(処理の構造化)と、 構造体の入れ子(データの構造化)の話から、構造化プログラミング(structured programming)といった話も紹介する。

オブジェクト指向プログラミング2015全講義録

オブジェクト指向プログラミングまとめ

2015年度のオブジェクト指向プログラミングの講義録を以下にまとめる。

レポート課題は以下のとおり

  • 隠蔽化に関するレポート(複素数クラスの直交座標系・極座標系での実装)
  • 仮想関数に関するレポート(図形クラスに機能を追加し色付き図形に拡張)
  • UMLに関するレポート(自分の特別研究の内容より2つのUML図を記述)

オブジェクト指向とソフトウェア工学

オブジェクト指向プログラミングの最後の総括として、 ソフトウェア工学との説明を行う。

トップダウン設計とウォーターフォール型開発

ソフトウェア工学でプログラムの開発において、一般的なサイクルとしては、 専攻科などではどこでも出てくるPDCAサイクル(Plan,Do,Check,Action)が行われる。 この時、プログラム開発の流れとして、大企業でのプログラム開発では一般的に、 トップダウン設計とウォーターフォール型開発が行われる。

トップダウン設計では、全体の設計(Plan)を受け、プログラムのコーディング(Do)を行い、 動作検証(Check)をうけ、最終的に利用者に納品し使ってもらう(Action)…の中で、 開発が行われる。設計の中身も機能仕様や動作仕様…といった細かなフェーズになることも多い。 この場合、コーディングの際に設計の不備が見つかり設計のやり直しが発生すれば、 全行程の遅延となることから、前段階では完璧な動作が必要となる。 このような、上位設計から下流工程にむけ設計を行う方式は、 トップダウン設計などと呼ばれる。また、処理は前段階へのフィードバック無しで次工程へ流れ、 川の流れが下流に向かう状態にたとえ、ウォーターフォールモデルと呼ばれる。

引用:Think IT 第2回開発プロセスモデル

このウォーターフォールモデルに沿った開発では、横軸時間、縦軸工程とした ガントチャートなどを描きながら進捗管理が行われる。

一方、チェック工程(テスト工程)では、 要件定義を満たしているかチェックしたり、設計を満たすかといったチェックが存在し、 テストの前工程にそれぞれ対応したチェックが存在する。 その各工程に対応したテストを経て最終製品となる様は、V字モデルと呼ばれる。

引用:@IT Eclipseテストツール活用の基礎知識

しかし、ウォーターフォールモデルでは、前段階の設計の不備があっても前工程に戻るという考えをとらないため、全体のPDCAサイクルが終わって次のPDCAサイクルまで問題が残ってしまう。 巨大プロジェクトで大量の人が動いているだから、簡単に方針が揺らいでもトラブルの元にしか ならないことから、こういった手法は大人数巨大プロジェクトでのやり方。

ボトムアップ設計とアジャイル開発

少人数でプログラムを作っている時(あるいはプロトタイプ的な開発)には、 部品となる部分を完成させ、それを組合せて全体像を組み上げる手法もとられる。 この方法は、ボトムアップ設計と呼ばれる。

また、ウォーターフォールモデルでは、前工程の不備をタイムリーに見直すことができないが、 少人数開発では適宜前工程の見直しが可能となる。 特にオブジェクト指向プログラミングを実践して隠蔽化が正しく行われていれば、 ライブラリの利用者への影響を最小にしながら、ライブラリの内部設計の見直しも可能となる。 このような内部構造の改善はリファクタリングと呼ばれる。

一方、プログラム開発で、ある程度の規模のプログラムを作る際、最終目標の全機能を実装したものを 目標に作っていると、全体像が見えずプログラマーの達成感も得られないことから、 機能の一部分だけ完成させ、次々と機能を実装し完成に近づける方式もとられる。 この方式では、機能の一部分の実装までが1つのPDCAサイクルとみなされ、 このPDCAサイクルを何度も回して完成形に近づける方式とも言える。 このような開発方式は、アジャイル開発と呼ぶ。 一つのPDCAサイクルは、アジャイル開発では反復(イテレーション)と呼ばれ、 短い開発単位を繰り返し製品を作っていく。この方法では、一度の反復後の実装を顧客に見てもらい、 顧客とプログラマーが一体となって開発が行われる。

引用:コベルコシステム

エクストリームプログラミング

アジャイル開発を行うためのプログラミングスタイルとして、 エクストリームプログラミング(Xp)という考え方も提唱されている。 Xpでは、5つの価値(コミュニケーション,シンプル,フィードバック,勇気,尊重)を基本とし、 開発のためのプラクティス(習慣,実践)として、 テスト駆動開発(コーディングではテストのためのプログラムも並行して行い,こまめにテストを実行する)や、 ペアプログラミング(2人ペアで開発し、コーディングを行う人とそのチェックを行う人で役割分担をし、 一定期間毎にその役割を交代する)などの方式が取られることが多い。

仮想関数と純粋仮想基底クラス

前々回の講義では派生と継承の説明にて、次のようなプログラムを説明した。

派生と継承の復習

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char* s , int a ) {
      strcpy( name , s ) ;
      age = a ;
   }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス
class Student : public Person {
private:
   char dep[ 10 ] ;
   int  grade ;
public:
   Student( const char* s , int a , const char* d , int g )
     : Person( s , a )
   {
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() { // 継承でなくStudent版のprintを定義
      Person::print() ;
      printf( "= %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // 派生と継承の確認
   Person tsaitoh( "tsaitoh" , 50 ) ;
   tsaitoh.print() ;
   Student naka( "naka" , 22 , "PS" , 2 ) ;
   naka.print() ; // Student版を実行
   // 異なる型のデータをごちゃ混ぜにできないか?
   Person* table[2] ;
   table[0] = &tsaitoh ;
   table[1] = &naka ;
   for( int i = 0 ; i < 2 ; i++ ) {
      table[i]->print() ;
   }
   return 0 ;
}

このプログラムのmain() では、tsaitohとnakaのデータを 表示させているが、tsaitohとnakaは、オマケの有る無しの違いはあっても、 元をたどれば、Person型。ごちゃまぜにできないだろうか?

int main() {
   :
   // 異なる型のデータをごちゃ混ぜにできないか?
   Person* table[2] ;
   table[0] = &tsaitoh ;
   table[1] = &naka ;
   for( int i = 0 ; i < 2 ; i++ ) {
      table[i]->print() ;
   }
   return 0 ;
}

C++では、派生クラスは格下げして基底クラスのように振る舞うこともできる。 上記の例では、Studentのポインタ型である、&nakaは、 Personのポインタ型として振る舞うこともできる。

ただし、table[]に格納する時点で、Person型へのポインタなので、 table[i]->print() を呼び出しても、Personとして振る舞うため、 tsaitoh 50 / naka 22 が表示される。

仮想関数

上記のプログラムでは、Student型には、名前,年齢,所属,学年をすべて表示する Student::print() が宣言してある。 であれば、「naka 22」 ではなく、「naka 22 PS 2」と表示したくはならないか?

上記の例では、table[] に格納する段階で、格下げされているため、「naka 22」しか表示できない。 しかし、以下のように書き換えると、「naka 22 PS 2」と表示できる。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char* s , int a ) {
      strcpy( name , s ) ;
      age = a ;
   }
   virtual void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス
class Student : public Person {
private:
   char dep[ 10 ] ;
   int  grade ;
public:
   Student( const char* s , int a , const char* d , int g )
     : Person( s , a )
   {
      strcpy( dep , d ) ;
      grade = g ;
   }
   virtual void print() { // 継承でなくStudent版のprintを定義
      Person::print() ;
      printf( "= %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   Person tsaitoh( "tsaitoh" , 50 ) ;
   Student naka( "naka" , 22 , "PS" , 2 ) ;
   // 異なる型のデータをごちゃ混ぜにできないか?
   Person* table[2] ;
   table[0] = &tsaitoh ;
   table[1] = &naka ;
   for( int i = 0 ; i < 2 ; i++ ) {
      table[i]->print() ;
   }
   return 0 ;
}

上記の例の用に、printメソッドに virtual 宣言を追加すると、 C++では、tsaitoh,naka のデータ宣言時に、データ種別情報が埋め込まれる。 この状態で、table[i]->print() を呼び出されると、 データ種別毎のprint()を呼び出してくれる。 (この実装では、前回授業の関数ポインタが使われている)

ここで、学生さんから、「table[] の宣言をStudentで書いたらどうなるの?」という 質問がでた。おもわず『良い質問ですねぇ〜』と池上彰ばりの返答をしてしまった。

Student* table[] ;
table[0] = &tsaitoh ; // 文法エラー
// 派生クラスは基底クラスにはなれるけど、
// 基底クラスが派生クラスになることはできない。
table[1] = &naka ;

純粋仮想基底クラス

// 純粋仮想基底クラス
class Object {
public:
   virtual void print() = 0 ; // 中身の無い純粋基底クラスを記述しない時の書き方。
} ;
// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() {
      printf( "%d\n" , data ) ;
   }
} ;
// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() {
      printf( "%s\n" , data ) ;
   }
} ;
// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() {
      printf( "%lf\n" , data ) ;
   }
} ;
// 動作確認
int main() {
   Object* data[3] = {
      new IntObject( 123 ) ,
      new StringObject( "abc" ) ,
      new DoubleObject( 1.23 ) ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      data[i]->print() ;
   }
   return 0 ;
} ;

この書き方では、data[]には、整数、文字列、実数という異なるデータが入っているが、 Objectという純粋仮想基底クラスを通して、共通な型のように扱えるようになる。 そして、data[i]->print() では、各型の仮想関数が呼び出されるため、 「123 abc 1.23」 が表示される。

ここで、Object の様な一見すると中身が何もないクラスを宣言し、 このクラスから様々な派生クラスを用いるプログラムテクニックは、 広く利用され、Objectのような基底クラスは、純粋仮想基底クラスなどと呼ばれる。

派生と継承

隠ぺい化の次のステップとして、派生・継承を説明する。

派生を使わずに書くと…

元となるデータ構造(例えばPersonが名前と年齢)でプログラムを作っていて、 途中でその特殊パターンとして、所属と学年を加えた学生(Student)という データ構造を作るとする。

// 元となる構造体(Person)                                                                                                                                                  struct Person {
   char name[ 20 ] ; // 名前
   int  age ;        // 年齢
} ;
// 初期化関数
void set_Person( struct Person* p ,
                 char s[] , int x ) {
   strcpy( p->name , s ) ;
   p->age = x ;
}
// 表示関数
void print_Person( struct Person* p ) {
   printf( "%s %d\n" , p->name , p->age ) ;
}
void main() {
   struct Person saitoh ;
   set_Person( &saitoh , "t-saitoh" , 50 ) ;
   print_Person( &saitoh ) ;
}

パターン1(そのまんま…)

上記のPersonに、所属と学年を加えるのであれば、以下の方法がある。 しかし以下パターン1は、要素名がname,ageという共通な部分があるようにみえるが、 プログラム上は、PersonとPersonStudent1は、まるっきり関係のない別の型にすぎない。

このため、元データと共通部分があっても、同じ処理を改めて書き直しになる。

// 元のデータに追加要素(パターン1)
struct PersonStudent1 {
   char name[ 20 ] ; // 名前
   int  age ;        // 年齢
   char dep[ 20 ] ;  // 所属
   int  grade ;      // 学年
} ;
void set_PersonStudent1( struct PersonStudent1* p ,
                         char s[] , int x ,
                         char d[] , int g ) {
   strcpy( p->name , s ) ; // 同じことを書いてる
   p->age = x ;
   strcpy( p->dep , d ) ;  // 追加分はしかたない
   p->grade = g ;
}
// 名前と年齢だけ表示
void print_PersonStudent1( struct PersonStudent1* p ) {
   // また同じ処理を書いてる
   printf( "%s %d\n" , p->name , p->age ) ;
}
void main() {
   struct PersonStudent1 naka1 ;
   set_PersonStudent1( &naka1 ,
   "naka" , 22 , "PS" , 2 ) ;
   print_PersonStudent1( &naka1 ) ;
}

パターン2(元データの処理を少し使って…)

パターン1では、同じような処理を何度も書くことになり、面倒なので、 元データ用の関数をうまく使うように書いてみる。

// 元のデータに追加要素(パターン2)
struct PersonStudent2 {
   struct Person person ;
   char          dep[ 20 ] ;
   int           grade ;
} ;
void set_PersonStudent2( struct PersonStudent2* p ,
                         char s[] , int x ,
                         char d[] , int g ) {
   // Personの関数を部分的に使う
   set_Person( &(p->person) , s , x ) ;
   // 追加分はしかたない
   strcpy( p->dep , d ) ;
   p->grade = g ;
}
void print_PersonStudent2( struct PersonStudent2* p ) {
   // Personの関数を使う。
   print_Person( &p->person ) ;
}
void main() {
   struct PersonStudent2 naka2 ;
   set_PersonStudent2( &naka2 ,
   "naka" , 22 , "PS" , 2 ) ;
   print_PersonStudent2( &naka2 ) ;
}

このパターン2であれば、元データ Person の処理をうまく使っているので、 プログラムの記述量を減らすことはできるようになった。

しかし、print_PersonStudent2() のような処理は、元データ構造が同じなのに、 いちいちプログラムを記述するのは面倒ではないか?

そこで、元データの処理を拡張し、処理の流用ができないであろうか?

基底クラスから派生クラスを作る

オブジェクト指向では、元データ(基底クラス)に新たな要素を加えたクラス(派生クラス)を 作ることを「派生」と呼ぶ。派生クラスを定義するときは、クラス名の後ろに、 「:」「public/protected/private」基底クラス名を書く。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x ) {
      strcpy( name , s ) ;
      age = x ;
   }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {
      strcpy( dep , d ) ;
      grade = g ;
   }
} ;
void main() {
   Person saitoh( "t-saitoh" , 50 ) ;
   saitoh.print() ;
   Student naka( "naka" , 22 , "PS" , 2 ) ;
   naka.print() ;
}

ここで注目すべき点は、main()の中で、Studentクラス"naka"に対し、naka.print() を呼び出しているが、パターン2であれば、print_PersonStudent2()に相当するプログラムを 記述していない。 しかし、この派生を使うと Person の print() が自動的に流用することができる。 これは、基底クラスのメソッドを「継承」しているから、 このように書け、名前と年齢「naka 22」が表示される。

さらに、Student の中に、以下のような Student 専用の新しい print()を記述してもよい。

class Student ...略... {
   ...略...
   void print() {
      Person::print() ;
      printf( "%s %d\n" , dep , grade ) ;
   }
} ;
void main() {
   ...略...
   Student naka( "naka" , 22 , "PS" , 2 ) ;
   naka.print() ;
}

この場合は、継承ではなく機能が上書き(オーバーライト)されるので、 「naka 22 / PS 2」が表示される。

派生クラスを作る際の後ろに記述した、public は、他にも protected , private を 記述できる。

public    だれもがアクセス可能。
protected であれば、派生クラスからアクセスが可能。
派生クラスであれば、通常は protected で使うのが一般的。
private   派生クラスでもアクセス不可。

隠蔽化の課題(複素数クラスを例に)

前回の隠蔽化の話を受け、実際のプログラムの例を課題に説明。 複素数クラスを(実部,虚部)で実装した後に、(絶対値,偏角)に直したら…

基本プログラム(実部と虚部)

複素数を扱うクラスを作るのであれば、基本的には以下の様なコードとなるだろう。 複素数どうしの簡単な加算・乗算を記載する。

class Complex {
private:
   double re , im ;
public:
   Complex( double x , double y ) {
      re = x ;
      im = y ;
   }
   // 上記コンストラクタは、以下のようにも書ける。
   // Complex( double x , double y )
   // :   re( x ) , im( y )
   // { メンバ以外の初期化... }
   void print() {
      printf( "%lf+j%lf¥n" , re , im ) ;
   }
   void add( Complex &z ) {
      re = re + z.re ;
      im = im + z.im ;
   }
   void mul( Complex &z ) {
      double x = re * z.re - im * z.im ;
      double y = re * z.im + im * z.re ;
      re = x ;
      im = y ;
   }
} ;
int main() {
   Complex a( 1 , 2 ) ;
   Complex b( 2 , 3 ) ;
   a.add( b ) ;
   a.print() ;
   a.mul( b ) ;
   a.print() ;
   return 0 ;
}

Complexクラス内部をリファクタリング

しかし、前述プログラムでは、mul()メソッドは、add()メソッドよりは、 複雑なものとなっている。 しかし、複素数の乗算は、(絶対値と偏角)を用いれば、絶対値の乗算・偏角の加算で 処理は簡単に記述できる。そこで、クラス内部を乗算と偏角で処理をするように変更してみる。

class Complex {
private:
   double r , th ;
public:
   Complex( double x , double y ) {
      r = sqrt( x*x + y*y ) ;
      th = atan2( y , x ) ; // atan2は象限を考慮してくれる
   }
   void print() {
      printf( "%lf ∠ %lf¥n" , r , th / 3.141592 * 180.0 ) ;
   }
   void add( Complex &z ) {
      // ここは面倒な式になっちゃう
   }
   void mul( Complex &z ) {
      r  = r  * z.r ;
      th = th + z.th ;
   }
} ;
int main() {
   Complex a( 1 , 2 ) ;
   Complex b( 2 , 3 ) ;
   a.add( b ) ;
   a.print() ;
   a.mul( b ) ;
   a.print() ;
   return 0 ;
}

ここで重要なポイントは、2つめの絶対値∠偏角のプログラムの呼び出し側 main() は、 1つめのプログラムとまるっきり同じである。

このように、オブジェクト指向の隠蔽化を行っていれば、当初のクラス設計が悪くて後で変更 したくなった場合、利用者側からの見た目の動作を変更せずに、内部のデータ構造や処理メソッドを 変更が可能となる。 このように、利用者側からの見た目を変更せずに処理の内部を変更すること、 リファクタリング と呼ぶ。これにより、プログラムの不備や問題点があっても、積極的にプログラムを 改良できることから、不備の少ない安全なプログラムを作れるようになる。

隠蔽化の課題

以上の2つのプログラムで複素数の計算メソッド、加算(add),除算(sub),乗算(mul),除算(div)…その他を (実部,虚部)、(絶対値,偏角)で記載し、適切に記述をすれば、呼び出し側main()を まるっきり同じものにできることを通して、隠蔽化についてレポートにまとめよ。

レポートでは、以下の点を記載すること。(レポートは、本科中間試験の頃までに提出が望ましい)

  • 2つの方式でのプログラム例
  • 上記プログラムに対する説明
  • 上記プログラムが正しく動作していたことが判る結果
  • この課題から判る考察

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー