オブジェクト指向の基本プログラム
C++のクラスで表現
前回の講義での、構造体のポインタ渡しをC++の基本的なクラスで記述した場合のプログラムを再掲する。
#include <stdio.h> #include <string.h> // この部分はクラス設計者が書く class Person { private: // クラス外からアクセスできない部分 // データ構造を記述 char name[10] ; // メンバーの宣言 int age ; public: // クラス外から使える部分 // データに対する処理を記述 void set( char s[] , int a ) { // メソッドの宣言 // pのように対象のオブジェクトを明記する必要はない。 strcpy( name , s ) ; age = a ; } void print() { printf( "%s %d¥n" , name , age ) ; } } ; // ← 注意ここのセミコロンを書き忘れないこと。 // この部分はクラス利用者が書く int main() { Person saitoh ; saitoh.set( "saitoh" , 55 ) ; saitoh.print() ; // 文法エラーの例 printf( "%d¥n" , saitoh.age ) ; // phoneはprivateなので参照できない。 return 0 ; }
この様にC++のプログラムに書き換えたが、内部の処理は元のC言語と同じであり、オブジェクトへの関数呼び出し saitoh.set(…) などが呼び出されても、set() は、オブジェクトのポインタを引数して持つ関数として、機械語が生成されるだけである。
用語の解説:C++のプログラムでは、データ構造とデータの処理を、並行しながら記述する。 データ構造に対する処理は、メソッド(method)と呼ばれる。 データ構造とメソッドを同時に記載したものは、クラス(class)と呼ぶ。 そのデータに対し具体的な値や記憶域が割り当てられたものをオブジェクト(object)と呼ぶ。
C++では隠蔽化をさらに明確にするために、private: や public: といったアクセス制限を指定できる。private: は、そのメソッドの中でしか使うことができない要素や関数であり、public: は、メソッド以外からでも参照したり呼出したりできる。オブジェクト指向でプログラムを書くとき、データ構造や関数の処理方法は、クラス内部の設計者しか触れないようにしておけば、その内部を改良することができる。しかし、クラスの利用者が勝手に内部データを触っていると、内部設計者が改良するとそのプログラムは動かないものになってしまう。
隠蔽化を的確に行うことで、クラスの利用者はクラスの内部構造を触ることができなくなる。一方でクラス設計者はクラスの外部への挙動が変化しないようにクラス内部を修正することに心がければ、クラス利用者への影響がないままクラスの内部を改良できる。このように利用者への影響を最小に、常にプログラムを修正することをリファクタリングと呼ぶ。
クラス限定子
前述のプログラムでは、class 宣言の中に関数内部の処理を記述していた。しかし関数の記述が長い場合は、書ききれないこういう場合はクラス限定子を使って、メソッドの具体的な処理をクラス宣言の外に記載する。
class Person { private: char name[10] ; int age ; public: // メソッドのプロトタイプ宣言 void set( char s[] , int a) ; void print() ; } ; // メソッドの実体をクラス宣言の外に記載する。 void Person::set( char s[] , int a ) { // Person::set() strcpy( name , s ) ; age = a ; } void Person::print() { // Person::print() printf( "%s %d¥n" , name , age ) ; }
inline 関数と開いたサブルーチン
オブジェクト指向では、きわめて簡単な処理な関数を使うことも多い。
例えば、上記のプログラム例で、クラス利用者に年齢を読み出すことは許しても書き込みをさせたくない場合、以下のような、inline 関数を定義する。(getterメソッド)
# 逆に、値の代入専用のメソッドは、setterメソッドと呼ぶclass Person { private: char name[10] ; int age ; public: // メソッドのプロトタイプ宣言 inline int get_age() { return age ; } // getter inline void set_age( int a ) { age = a ; } // setter } ;ここで inline とは、開いた関数(開いたサブルーチン)を作る指定子である。通常、機械語を生成するとき中身を参照するだけの機械語と、get_age() を呼出したときに関数呼び出しを行う機械語が作られる(閉じたサブルーチン)が、age を参照するだけのために関数呼び出しの機械語はムダが多い。inline を指定すると、入り口出口のある関数は生成されず、get_age() の処理にふさわしい age を参照するだけの機械語が生成される。
# 質問:C言語で開いたサブルーチンを使うためにはどういった機能があるか?
コンストラクタとデストラクタ
プログラムを記述する際、データの初期化忘れや終了処理忘れで、プログラムの誤動作の原因になることが多い。
このための機能がコンストラクタ(構築子)とデストラクタ(破壊子)という。
コンストラクタは、返り値を記載しない関数でクラス名(仮引数…)の形式で宣言し、オブジェクトの宣言時に初期化を行う処理として呼び出される。デストラクタは、~クラス名() の形式で宣言し、オブジェクトが不要となる際に、自動的に呼び出し処理が埋め込まれる。
class Person { private: // データ構造を記述 char name[10] ; int age ; public: Person() { // (A) 引数なしのコンストラクタ name[0] = 'class Person { private: // データ構造を記述 char name[10] ; int age ; public: Person() { // (A) 引数なしのコンストラクタ name[0] = '\0' ; age = 0 ; } Person( char s[] , int a ) { // (B) 引数ありのコンストラクタ strcpy( name , s ) ; age = a ; } ~Person() { // デストラクタ print() ; } void print() { printf( "'%s' = %d¥n" , name , age ) ; } } ; int main() { Person saitoh( "saitoh" , 55 ) ; // オブジェクトsaitohを"saitoh"と55で初期化 Person tomoko ; // 引数なしのコンストラクタで初期化される。 return 0 ; // main を抜ける時にオブジェクトsaitohは不要になるので、 // デストラクタが自動的に呼び出され、'saitoh' = 55 が表示。 // 同様に tomoko のデストラクタでは、'' = 0 を表示。 }' ; age = 0 ; } Person( char s[] , int a ) { // (B) 引数ありのコンストラクタ strcpy( name , s ) ; age = a ; } ~Person() { // デストラクタ print() ; } void print() { printf( "'%s' = %d¥n" , name , age ) ; } } ; int main() { Person saitoh( "saitoh" , 55 ) ; // オブジェクトsaitohを"saitoh"と55で初期化 Person tomoko ; // 引数なしのコンストラクタで初期化される。 return 0 ; // main を抜ける時にオブジェクトsaitohは不要になるので、 // デストラクタが自動的に呼び出され、'saitoh' = 55 が表示。 // 同様に tomoko のデストラクタでは、'' = 0 を表示。 }
このクラスの中には、(A)引数無しのコンストラクタと、(B)引数ありのコンストラクタが出てくる。C++では、同じ名前の関数でも引数の数や型に応じて呼出す関数を適切に選んでくれる。(関数のオーバーロード)
デストラクタは、データが不要となった時に自動的に呼び出してくれる関数で、一般的にはC言語でのファイルの fopen() , fclose() のようなものを使う処理で、コンストラクタで fopen() , デストラクタで fclose() を呼出すように使うことが多いだろう。同じように、コンストラクタで malloc() を呼出し、デストラクタで free() を呼出すというのが定番の使い方だろう。
複素数クラスの例
隠蔽化と基本的なオブジェクト指向の練習課題として、複素数クラスをあげる。ここでは、複素数の加算・乗算を例に説明をするので、減算・除算などの処理を記述することで、クラスの扱いに慣れてもらう。
直交座標系の複素数クラス
#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 ) { // 初期化はこのように書いてもいい } // 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 ; }
練習課題
- 上記の直交座標系の複素数のクラスのプログラムを入力し、動作を確認せよ。
- このプログラムに減算や除算の処理を追加せよ。
この練習課題は、次週に予定している「曲座標系の複素数クラス」に変更となった場合のプログラムを加え、第1回のレポート課題となります。