データ構造を扱うプログラムの書き方を説明してきたが、その考え方をプログラムにするためには手間もかかる。こういった手間を少しでも減らすために、プログラム言語が支援してくれる。その代表格がオブジェクト指向プログラミング(Object Oriented Programming:略称OOP)であり、以下にその基本を説明する。
データ指向のプログラム記述
名前と年齢のデータを扱うプログラムをC言語で書く時、私なら以下のようなプログラムを作成する。
このプログラムの書き方では、saitohというデータにset_NameAge() , print_NameAge() を呼び出していて、データに対して処理を加えるという雰囲気がでている。(C言語なのでデータに処理を施す関数には、必ずどのデータに対する処理なのかを与えるポインタがある。) このようにプログラムを書くと、saitoh というデータに対して命令するイメージとなり、擬人化したデータに向かってset,printしろ…って命令しているように見える。
// 名前と年齢の構造体
struct NameAge {
char name[ 20 ] ;
int age ;
} ;
// NameAgeを初期化する関数
void set_NameAge( struct NameAge* p , char s[] , int a ) {
strcpy( p->name , s ) ;
p->age = a ;
}
// NameAgeを表示する関数
void print_NameAge( struct NameAge* p ) {
printf( "%s %d¥n" , p->name , p->age ) ;
}
void main() {
struct NameAge saitoh ;
set_NameAge( &saitoh, "t-saitoh" , 53 ) ;
print_NameAge( &saitoh ) ;
// NameAge の中身を知らなくても、
// set_NameAge(),print_NameAge() の中身を見なくても、
// saitoh を set して print する....という雰囲気は伝わるよね!!
}
このプログラムでは、例えば、データに誕生日も覚えたいという改良を加えるとしても、main の前のデータ構造と関数の部分は色々と書き換えることになるだろうけど、main の内部はあまり変わらないだろう。こういう書き方をすればプログラムを作成するときには、データ構造とそれを扱う関数を記述する人と、データ構造を使う人(main内部を書く人)と、分業ができるようになる。
隠蔽化
このような記述では、データ構造の中身を知らなくても、main で、setしてprintして…という処理の雰囲気は分かる。さらに、set_NameAge()とか、print_NameAge() の処理の中身を知らなくても、設定するとか表示するとか…は予想できる。
これは、NameAge というデータをブラックボックス化(隠蔽化)して捉えていると見れる。データ構造の中身を知らなくてもプログラムを理解できることは、データ構造の隠蔽化という。また、関数の中身を知らなくても理解できることは、手続きの隠蔽化という。
オブジェクト指向プログラミング
前述のように、プログラムを書く時には、データ構造とそのデータを扱う関数を一緒に開発する方が分かり易い。そこで、プログラム言語の文法自体を、データ構造とその関数(メソッドと呼ぶ)をまとめてクラスとして扱うプログラムスタイルが、オブジェクト指向プログラミングの基本である。
class NameAge {
private:
// データ構造の宣言
char name[ 20 ] ;
int age ;
public:
// メソッドの定義
void set( char s[] , int a ) { // 初期化関数
strcpy( name , s ) ; // どのデータに対する処理かは省略できるので、
age = a ; // データへのポインタ引数は不要。
}
void print() { // 表示関数
printf( "%s %d¥n" , name , age ) ;
}
} ;
void main() {
NameAge saitoh ;
saitoh.set( "t-saitoh" , 53 ) ; // set,printはpublicなので自由に使える。
saitoh.print() ;
// saitoh.age = 54 ; エラー:クラス外でprivateの要素は触れない。
}
このプログラムでは、saitoh というデータ(具体的なデータが割り当てられたものはオブジェクトと呼ぶ)に対して、set() , print() のメソッドを呼び出している。
# C++ではクラス毎に関数名を区別してくれるので、関数名もシンプルにset,printのようにかける。
オブジェクト指向では、データに対して private を指定すると、クラス以外でその要素やメソッドを扱うことができなくなる。一方 public が指定されたものは、クラス外で使っていい。これにより、クラスを設計する人と、クラスを使う人を明確に分けることができ、クラスを使う人が、クラス内部の変数を勝手に触ることを禁止できる。
プログラムを記述する時には、データ件数を数える時に、カウンタの初期化を忘れて動かないといった、初期化忘れも問題となる。オブジェクト指向のプログラム言語では、こういうミスを減らすために、データ初期化専用の関数(コンストラクタ)を定義することで、初期化忘れを防ぐことができる。
// コンストラクタを使う例
class NameAge {
// 略
public:
NameAge( char s[] , int a ) { // データ初期化専用の関数
strcpy( name , s ) ; // コンストラクタと呼ぶ
age = a ;
}
// 略
} ;
void main() {
NameAge saitoh( "t-saitoh" , 53 ) ; // オブジェクトの宣言と初期化をまとめて記述できる。
saitoh.print() ;
}
プログラムにオブジェクト指向を取り入れると、クラスを利用する人とクラスを記述する人で分業ができ、クラスを記述する人は、クラスを利用するプログラマーに迷惑をかけずにプログラムを修正できる。
この結果、クラスを記述する人はプログラムを常により良い状態に書き換えることができるようになる。このように、よりよく改善を常に行うことはリファクタリングと呼ばれ、オブジェクト指向を取り入れる大きな原動力となる。。
最近のC++なら
最近のオブジェクト指向プログラミングは、テンプレート機能と組み合わせると、単純リスト処理が以下のように書けてしまう。struct 宣言やmalloc()なんて出てこない。(^_^;
#include <iostream>
#include <forward_list>
#include <algorithm>
int main() {
// std::forward_list<>線形リスト
std::forward_list<int> lst{ 1 , 2 , 3 } ; // 1,2,3の要素のリストで初期化
// リスト先頭に 0 を挿入
lst.push_front( 0 ) ;
// 以下のような処理を最新のC++なら...
// * もともとのC言語なら以下のように書くだろう。
// for( struct List*p = top ; p != NULL ; p = p->next )
// printf( "%d¥n" , p->data ) ;
// * 通常の反復子iteratorを使って書いてみる。
// auto は、lst の型推論。
// ちょっと前のC++なら型推論がないので、
// std::forward_list<int>::iterator itr = lst.begin() と書く。
// * C++では演算子の処理をクラス毎に書き換えることができる。
// itr++ といっても、カウントアップ処理をする訳ではない。
for( auto itr = lst.begin() ;
itr != lst.end() ;
itr++ ) {
std::cout << *itr << std::endl ;
}
// 同じ処理を algorithm を使って書く。
std::for_each( lst.begin() ,
lst.end() ,
[]( int x ) { // 配列参照のコールバック関数
std::cout << x << std::endl ;
} );
// 特に書かなくてもデストラクタがlstを捨ててくれる。
return 0 ;
}
テンプレート機能
テンプレート機能は、実際のデータを覚える部分の型を後で指定できるようにしたデータ構造を定義する機能。
template <class > struct List { T data ; struct List* next ; } ; int main() { List<int> li ; // 整数を要素とするList型の宣言 List<double> ld ; // 実数を要素とするList型の宣言 }