9. アプリケ−ション作成のための指針 9-1. C++でのプログラミング (1) オブジェクト指向プログラミングって結局何なのか しばらく前、Cや Pascal 言語で構造化プログラミングということが言われた時代があっ た。しかし現在のC++程の言われようでは無かった。もちろんその当時、構造化プログ ラミングに対しパラダイムなんて哲学めいた言葉は、使われなかったように記憶している。 ただ我々プログラマが成すことは、C言語の struct 文を理解することと、ポインタを理 解することが全てだった。また、実際のところこれ以外考える必要もなかった。 -------------------------------------------------------------------------------- 構造化プログラミングというのは、C言語では即ち構造体を使うこと、そのものだった。 -------------------------------------------------------------------------------- 異論のある方もいるだろうが、少なくとも著者の経験からはそうであったと言える。そこ で、ではオブジェクト指向プログラミングとは一体何だと考えてみると、 -------------------------------------------------------------------------------- オブジェクト指向プログラミングとは、クラスを使ってプログラムを作成することである。 -------------------------------------------------------------------------------- この一語に集約されるのでないか。エンジニアの世界で、哲学めいた長々とした説明が必 要なものは本当ではない。すっぱり言い切れるものでなくてはならないはずだ。どんどん クラスを使って、プログラミングして行く。それだけのことだし、それが全てなのである。 (2) 勘どころ * デ−タ構造をどう考えるのか C++では従来の意味で、デ−タ構造を考えることはあまり重要ではない。従来のアプリ ケ−ション設計では、先ず初めにデ−タ構造ありきで、次にそのデ−タをアクセスする関 数を考えた。デ−タ構造を設計することが、ほとんどアプリケ−ション設計の中心的な課 題であり、関数は、どちらかと言うとデ−タ構造の補助的な存在のような感じさえあった。 C++では、ある機能単位にデ−タと関数の両者を含めた構造、クラスを考えることが重 要となる。そして、それがそのままデ−タ構造を設計することになると言える。また、ク ラスから生成されたオブジェクトの集まりと、それを全体的に管理するクラスを含めたも のを、デ−タベ−スとして考えることができる。 * クラス階層をどう考えるのか アプリケ−ションのクラスを設計するに当たっては、必要となる個々のクラスを設計する ことが重要であって、クラス階層を構築すること自体が本質的な問題ではない。ただクラ ス階層を利用してクラス設計すると、設計が楽になるし、間違いも少なくなるし、便利だ よと言うことである。 クラス階層の深さは、多分出来れば浅い方がいい。あまり浅いとクラス階層の意味がなく なってしまうが、深くて継承したデ−タやメンバ関数が分からなくなるよりはましである。 Motif や InterViews などの例を参考にすると、3〜4階層ぐらいにしておくのが望まし いと思われる。 * メッセ−ジをどう考えるか メッセ−ジ・パッシングという用語がある。メッセ−ジによって、どんどんオブジェクト が起動されて行くイメ−ジを想像するが、メッセ−ジ処理を終えた前のオブジェクトは一 体どうなっているのか。{b} の処理をしている時aは、役目が済んだと寝ているのだろう か。実は {b} や {c} は {a} のスコ−プ内にあって、 関数の入れ子の状態になっている のである。このため {c} の処理が終わると {b} に戻り、 {b} の処理が終わると {a} に 戻る。即ち、最初に起動したオブジェクトに制御は戻るのである。{a} → {b} → {c} と 行きっぱなしになるわけではない。 class A { class B { class C { B* b ; C* c ; public: public: public: void funcC() { {a} → {b} → {c} void funcA() { void funcB() { cout << "C"; ↑ b->funcB() ; c->funcC() ; } メッセ−ジ cout << "A" ; cout << "B" ; } } } } } (3) プログラム作成の実践方法 * クラス作成のプログラミングについて C++でのプログラミングはオブジェクト指向の名の元、クラスをどんどん作成していく ことになる。他のファイルで定義されたクラスを使用する場合には、クラスの定義部をそ のファイルにインクル−ドしてやる必要がある。 このため通常クラスの作成は、クラスの定義部とメンバ関数の実装部に分けてプログラミ ングすることになる。定義部は xxx.h ファイルに、メンバ関数部は xxx.C ファイルとい う具合である。そうすると基本的には、クラス1個につき2つのファイルが必要となる。 プログラミング作業において、2つのファイルをいつも見なければならないのは不便であ る。そのため定義部とメンバ関数部は1つのファイルにし、プリ・プロセッサと専用コン パイル・スクリプトで、クラス定義部用ファイルを自動作成することを考えた。 * プログラム作成の手順 1) クラスの定義部とメンバ関数の実装部を、全て記述したファイルを作成する。 2) クラスの定義部を #if 1 と #endif で囲む。 3) 専用コンパイルスクリプトで、クラスの定義部のみのファイルを自動作成する。 $ cpc sample -h << これで sample.h というファイルができる。 サンプルプログラムとして、z0.C,z1.C,z2.C などが入っています。 4) 後は通常通り、コンパイルしてリンクする。 sample.C --------------------------------------- |#include | |// ------ クラス定義部 ------ |#if 1 // クラスの定義部を #if 1 と #endif で囲む。 |class PPP { | int n ; |public: | PPP() {} | void set( int j ) ; | inline void print() ; // インライン指定に注意! |} ; |#endif | |// ------ クラス実装部 ------ |void PPP::set( int j ) { n = j ; } | |#if 1 //インライン指定したメンバ関数も #if 1 指定すること。 |inline void PPP::print() { cout << n << endl ; } |#endif program.C --------------------------------------- |#include |#include "sample.h" | |main() |{ | PPP p ; | p.set( 10 ) ; p.print() ; |} * cpc と exhead スクリプト cpc << すいません。これは Apollo の Aegis Shell です。 ------------------------------------------------------------ |if eqs ^1 '' then | args "[ *** C++コンパイル用スクリプト << cpc >> *** ]" | args "" | args " オブジェクトファイルを xxx.o で、カレント・ディレ" | args " クトリに作成する。拡張子は xxx.C のこと。 " | args "" | args " $ cpc test ただコンパイルするだけ。 " | args " $ cpc test -h if 0 .. endif で指示されたファイ" | args " ルの部分をヘッダ−部を test.h で" | args " 作成する。 " | args "--------------------------------------------------" | exit |endif |if eqs ^2 '' then | CC ^1.C -A inlib,/lib/xmlib1.1 -c -b ^1.o -v |endif |if eqs ^2 '-h' then | exhead ^1.C |endif exhead ------------------------------------------------------------ |#!/bin/csh |# C++の xxx.C ファイルからヘッダ−部だけ抜き出して、 |# xxx.h ファイルを自動的に作成するスクリプト。 | |set k = $1:r | |if ! -f $1 then | echo "ソ−スファイルが見つかりません" | exit |endif | |# ソ−スの拡張子が .C かチェックする |if $1:e != "C" exit | |echo "#ifndef $k""_h" > $k.h |echo "#define $k""_h" >> $k.h |echo "" >> $k.h | |awk /"if 1"/,/endif/ $1 >> $k.h | |echo "" >> $k.h |echo "#endif" >> $k.h | |echo "ヘッダ−ファイルを作成しました" 9-2. C言語とのデ−タ構造の比較 (1) C言語の構造体の場合 C言語でデ−タ構造を考える場合、ほとんど構造体とポインタを用いた、線形リスト構造 であった。ちなみに Fortran では、配列がデ−タ構造を表わす唯一の方法だった。 線形 リストは構造体の種類分だけできるのが普通であり、結局線形リストの集まりが、C言語 でのデ−タ構造そのものであるとも言える。 1つの構造体には格納するデ−タの−組が入っていて、デ−タの数だけ芋ずる式につなが っているというイメ−ジである。決して個々のデ−タが独立して存在しているというイメ −ジではない。またここに示したデ−タ集合は、ただのデ−タの集まりに過ぎない。デ− タを管理する関数群は、デ−タ集合とは別に定義されることになる。 ----------------------- --------------- 線形リスト | [L]→[L]→NULL |――― | |――― | |\ | | | | | | ↓ * | ――→| | ――→ NULL | [P]→[P]→[P]→NULL | Ponter | | ----------------------- --------------- デ−タ集合1 デ−タ集合2 (2) C++のオブジェクトの場合 一方オブジェクト指向では、オブジェクト1つ1つがデ−タそのものであると考える。オ ブジェクトは構造体とは違って、デ−タだけでなく、そのデ−タをアクセスする手続きも 持つ、1個の完結した機能体である。このため基本的には、オブジェクトを外から管理す るような構造は必要ないのである。ただ、オブジェクト全てに共通な処理をしようとする 場合などには、何らかのオブジェクトを管理しておく集合が必要となる。この集合は何で もいいわけであり、例えば線形リストにしておこうかという話しになるだけである。 またC言語の線形リストは同種の構造体を連結するのに対し、C++での線形リストは一 般的には、異種のオブジェクトを管理することが大きく異なっている。これはC++の仮 想関数の働きによるところが大きい。デ−タ管理集合に線形リストを取り上げたが、配列 でもスタックでも同様なことが言える。 大きな丸のつもり ________________ {L}や{P} はオブジェクト ________________ / *{P} \ である。大きな丸もオブジ / \ | / | ェクトとである。 | | | {L} → {P} | | | | | * | | | | / の印しも → と同じ | | | *{P} | くポインタを表わす。 | | | / | | | | {L} → {P} | | | \________________/ \________________/ デ−タ管理集合1 デ−タ管理集合2 9-3. オブジェクトの分類 (1) オブジェクトを"もの"として見る場合 一般的にオブジェクト指向において、オブジェクトとは何々するものという概念にあては まるものと言える。そして、そのオブジェクトは普通、幾つかあって何かの働きをするも のである。たとえ1個しかないものであっても、"もの"としてのオブジェクトになる。例 えばコンピュ−タのマウスは1個しかなく、しかもその管理はOSがやっている。1個し かないのだから、わざわざオブジェクトにする必要もないのでないか。そう思ってしまう が、システムコ−ルでマウスのカ−ソルのON/OFFや移動を制御すると考えれば、オ ブジェクトにするメリットが見えてくるのではないだろうか。 またオブジェクトが幾つも集まって、さらに1個の"もの"としてのオブジェクトになるこ ともある。例えば人間の体にたとえれば、内臓の器官1つ1つが1個の機能を持っていて、 それらが有機的に結び付いてその上の器官になり、その集合として人体を形づくっている。 これはまさにシステムであり、システムの部品はオブジェクトであり、システム自体もオ ブジェクトであると言える。 ただし、注意しなければならないのは人体の例は、オブジェクト指向においてはオ−バ− スペックになるということである。人体の器官は同時に働いているわけであって、プログ ラミングのオブジェクトは1つ1つしか働かないのである。つまりオブジェクトはメッセ −ジ、即ち相手オブジェクトの関数の起動により、働くオブジェクトの制御が移って行く という過程をとる。 従ってオブジェクト指向で扱うオブジェクトは、ある時点で同時に複数のオブジェクトが 変化しないもの、あるいはそう見なして差し支えないものに限られることになる。限られ ると表現したが、適用できない場面は少ないかも知れない。人体の場合でも、ある器官か らホルモンが出て、それを脊髄が受けて、それからと言うように連鎖反応の範囲で考えれ ば適用できることになる。 (2) オブジェクトを"知識"として見る場合 クラスを知識の格納庫とみなすと、"知識"をクラスの継承機能を用いてうまく表現するこ とができる。これは人工知能で言われる『フレ−ム』のような知識構造を構築することに 相当する。実装に当たって考慮することは、どうやって"知識"を保持しているオブジェク トを登場させるかである。オブジェクトを生成するまでもなく、静的なクラスにしておく、 あるいはOODB(後述)でオブジェクトをコンピュ−タ内に保存してしまうか、泥臭くプ ログラム実行時にオブジェクトをそのつど生成するか。いずれかの方法を取らなければな らない。 |- 分類上の概念としての知識 → 固定しているため、クラスに内臓すべきである。 | オブジェクトはただ1つあればよい。 知識 --| | |- 事実や現象としての知識 → 変化するため、オブジェクトがもつべきである。 オブジェクトは幾つもできる。 * "分類上の概念としての知識" の例 形状 --- 平行四辺形 --- 正方形 | | | |- 菱形 個々の要素はクラスを表わしている。オ | ブジェクトではない。これらは、ここで |- 3角形 ------- 正3角形 はものの性質を表わしているのであって、 | グラフィックスの実際に表示されるよう |- 2等辺3角形 なものではない。 * 参考 フレ−ムの話しを読んでいると IS-A, A-KO などと用語がでてくる。IS-Aは is-a で何々 は何々である、それにA-KO は a-kind-of(または is-kind-of)で、何々は何々の一種で あるを表わしている。ついで has-a と part-of もあるが、これらは同じ意味で、何々は 何々の一部であるということである。どうもこれらの用語は本によってまちまちで、混乱 しているよう見える。 IS-A の関係 A-KO の関係 C++との対応では < >が < パンダ > 抽象的な概念 < 動物 > 両方とも抽 クラス、{ } で囲んだのが | | 象的な概念 オブジェクトになる。 | | {トントン} 実際のもの < パンダ > 9-4. クラス階層について (1) クラスの分類 次の図はクラス定義の性質によって、どのようなクラスの種類が考えられるか示したもの である。一応この分類は、クラスから生成されるオブジェクトは"もの"として考えた。以 下の何々クラスという名称は、小生が勝手につけたものである。クラス名称の意味は、続 く例を参考にして頂きたい。 |- 専用クラス |- 基礎クラス --| | |- 共有クラス |- 基礎クラス--| | | |- 機能限定型派生クラス |- 基礎クラス--| |- 派生クラス --| | | |- 機能追加型派生クラス | |- 拡張クラス--| | クラス--| | |- 機能共通型派生クラス | | | |- 共通クラス | | | |- 属性拡張型派生クラス |- 抽象クラス- |- 概念クラス | | |- 機能拡張型派生クラス |- 仮想クラス * "基礎クラス"は、意味のあるインスタンスを生成できるクラスをいう。 * "拡張クラス"は、基礎クラスをベ−スに属性や機能を拡張したクラスをいう。 * "共通クラス"は、グラフィックスを例にすると線分や円などに共通な性質を集めたクラ スをいう。 * "概念クラス"は、グラフィックスを例にするとグラフィックス全体で定義されるような 抽象的なクラスをいう。 * "仮想クラス"は、C++で多態性を実現するためのみのクラスである。 * 抽象クラスの"共通クラス"と"概念クラス"は、インスタンスを生成できないこともない が、意味はない。"仮想クラス"は、仮想関数が0で定義されているクラスで、インスタ ンスは全っく生成できない。 [基礎クラスの例] グラフィックスで円と円弧のオブジェクトを最終的に生成するために、どんなクラス階層 が考えられるか列挙してみる。< >はクラス、{ }で囲んだものはオブジェクトを表わす。 *{arc} / [a] → {circle} 機能限定型派生クラス *{circle} / [b] → {arc} 機能追加型派生クラス |- → {circle} 機能共通型派生クラス [c] --| |- → {arc} Base は 共通クラス *{circle} / [d] 共有クラス(オブジェクトを生成する時に分ける) \ *{arc} [e] → {circle} 専用クラス → {arc} [拡張クラスの例] [f] |- 属性拡張型派生クラス --| |- 機能拡張型派生クラス [g] Circle a( x,y ) ; これは基礎クラス Colored_Circle b( x,y,YELLOW ) ; Moved_Circle c( x,y ) ; c.move( x1,y1 ) ; * 参考:基礎クラスの例の実装例 [a] clas Arc { class Circle: public Arc { int x,y,r,sdeg,edeg ; public: public: Circle( ix,iy,ir ) { Arc( ix,iy,ir,isdeg,iedeg ) { x = ix ; ... x = ix ; ... sdeg = 0 ; edeg = 360 ; } } } ; } ; [b] clas Circle { class Arc: public Circle { int x,y,r ; public: public: Arc( ix,iy,ir,isdeg,iedeg ) { Circle( ix,iy,ir ) { x = ix ; ... x = ix ; ... } } } ; } ; [c] class Base { class Circle: public Base { class Arc: public Base { protected: int r ; int sdeg,edeg,r ; int x,y ; public: public: } ; Circle( ix,iy,ir ) { Arc( ix,iy,ir,isdeg,iedeg ) { x = ix ; r = ir ; x = ix ; sdeg = isdeg ; .. } } } ; } ; [d] class Circle_Arc { int x,y,r,sdeg,edeg ; public: Circle_Arc( ix,iy,ir ) { このコンストラクタを使うと Circle sdeg = 0 ; edeg = 360 ; オブジェクトを生成する } Circle_Arc( ix,iy,ir,isdeg,iedeg ) { こっちのコンストラクタを使うと Arc x = ix ; ... オブジェクトを生成する } } ; * 参考:クラス設計の善し悪し 多分このテ−マだけでも10ペ−ジぐらいのボリュ−ムになるかと思われるが、1つだけ 派生クラスの設計指針となる実際のプログラム例を示す。クラスを派生する際の基底クラ スのメンバ変数の隠蔽度について示した。実際のアプリケ−ションでは、この他にメンバ 関数の扱い、仮想関数をどう設定するか、また純粋仮想関数にするのかただの仮想関数に するのか、実に多くの事柄を検討することになるだろう。 しかし最も関心を払うべきことは、それぞれのクラスを独立性が高くなるように設計する ことだろう。フレンド関数を多用して、結局どこからでもアクセスできてしまうようなこ とは避けるべきである。即ちクラスをオブジェクト指向でいう所のオブジェクトらしく設 計すること、このことがポイントになる。仮想関数や継承機能(派生クラス)などは、オ ブジェクトの設計においては、C++の言語仕様における便利な機能でしか過ぎない。あ まりこれらの機能に気を取られて、本質から外れないように注意することが肝心である。 [1] 良いクラスの設計 class Arc { 最初 Arc を記述したとしよう。 当然 s_deg,e_deg,radius int s_deg ; はプライベ−ト変数と定義する。次に Arc の派生クラスと int e_deg ; して Circle を記述する。Circle では radius だけ関係す protected: るので、 使えるように Arc 側で radius を protected に int radius; 変更したという筋書になる。 public: Arc( int sdeg,int edeg,int ir ) { s_deg = sdeg ; e_deg = edeg ; radius = ir ; } } ; Circle クラスからは必要のない Arc の角度は見えない。 class Circle: public Arc { public: Circle( int ir ):Arc(0,360,ir) { radius = ir ; } } ; [2] 良くないクラスの設計 class Arc2 { Circle クラスには必要ない s_deg,e_deg まで Arc 側で開 protected: 放してしまっている。メンバ変数を全部 protected 指定す int s_deg ; るのが、派生クラスを作成するのに一番容易なやり方である。 int e_deg ; int radius; public: Arc2() {} Arc2( int sdeg,int edeg,int ir ) { s_deg = sdeg ; e_deg = edeg ; radius = ir ; } } ; class Circle2: public Arc2 { public: Circle2( int ir ) { s_deg = 0 ; e_deg = 360 ; radius = ir ; } } ; (2) クラス階層の構成 下の図は一般的なアプリケ−ションにおいて、どのようなクラスの構造をとるか示したも のである。これは例であって、必ずこのような階層になるとは限らない。例えば仮想クラ スは概念クラスと一緒になって区別されない場合も出てくるかもしれない。多重継承によ り定義されるクラスについても、もう少し考察すると、ある種の場合分けが出来るかも知 れない。 |- 基礎クラス (機能共通型派生クラス) |- 共通クラス --| | |- 基礎クラス 仮想クラス -- 概念クラス --| |- 共有クラス | サ−ビスクラス |- 基礎クラス (専用クラス) | |- 基礎クラス ---- 基礎クラス (機能限定、 | 追加型派生クラス) | |- 基礎クラス ---- 基礎クラス (拡張型派生クラス) | | | |- 基礎クラス | |- 基礎クラス --| 多重継承により基礎クラス、共有クラ | |- ス、共通クラス、概念クラスのいずれ |- 基礎クラス --| かに成り得る。 or 共有クラス サ−ビスクラスはクラス階層に属さない、単独のクラスまたは他のクラス階層に属すクラ スであり、クラス階層中のクラスと結び付いて働くクラスをいう。既存のクラス・ライブ ラリを利用する場合など、自分のクラスと無関係に使えるクラスであればサ−ビスクラス と言ってよい。 グラフィックスのクラス設計を例に、クラス階層を考えてみる。基礎クラスは線分や円を 表示したり、座標を保存するオブジェクトのクラスである。概念クラスは、ここでは線分 や円といったオブジェクトに共通な属性を保存したり、アクセスすしたりするクラスにな る。仮想クラスは、今ここでは本当に純粋仮想関数しかないので、分かり易いだろう。サ −ビスクラスの Vector は、他のクラスとは全く独立している。ベクトル計算を簡単にす るために設けたクラスでしかない。 [仮想クラス] class Entity { [サ−ビスクラス] public: virtual void show() = 0 ; class Vector { } ; ..... } ; [概念クラス] class Basic: public Entity { protected: int color ; public: int get_color() { return color ; } } ; [基礎クラス] class Line: public Basic { Vector *s,*e ; public: Line( Vector* a,Vector* b,int c ) { s = a ; e = b ; color = c ; } void show() { cout << "I am Line\n" ; } } ; (3) クラス階層作成の指針 先にクラスの派生の仕方によって、色々考えられることを示した。しかし実際、クラス階 層を作成する場面で、これはどのクラスに属するのか考えたりできるものでない。そこで、 簡単にクラスを作成する際の参考となる指針を与えたい。 [ 共通部分をまとめる場合 ]−後向きの派生の仕方 それぞれ完結した機能を持つ、既存クラスの共通部分を、基底クラスにしてしまう場合で ある。この場合基底クラスは、基底クラスだけで完結した機能を持つことができなくなる。 * 既存クラスをそのまま使う場合−車を何種類も設計したあとで、 良く見たらシャシ−部 分が同じだった。 そこでシャシ−の図面を共通化した。 * 既存クラスを少し修正する場合−車を何種類も設計した。 その後良く見たら少しの修正 で、シャシ−部分を同じにできることが分かった。 そ こでシャシ−の図面を修正し共通化した。 ---------- ---------- |////////| |////////| A と B の中に同じデ−タや A |--------| B |--------| 機能があるぞ。 | | | | ---------- ---------- ↓ ---------- ---------- A"|////////| B"|////////| 同じ部分を分離して考えてみ ---------- ---------- よう ---------- ---------- A'| | B'| | ---------- ---------- ↓ ---------- A"=B" 同じ部分を1つにまとめてし C |////////| まおう。但し分けた所を、関 ---------- 連付けだけは、しておかない / \ といけないな。 * * ---------- ---------- A'| | B'| | ---------- ---------- この場合 C は A'と B'で共通に使うものであって、A'や B'が無くて、C だけあっても仕 方ないものである。A'、B' に対して C を基本クラスだとか基底クラスだとか呼び名はあ るが、意味的には A'、B' の共通クラスと言うことになる。 また C だけ有っても何も成 さないと言う意味で、C を特に抽象クラスとも呼ぶ。 [ 機能の追加をする場合 ]−前向きの派生の仕方 完結した機能を持つ既存クラスに対し、機能を追加したクラスを作成する場合である。そ れまでの既存クラスは、派生クラスの基礎部分としてのクラスと言うことになる。 * 既存クラスをそのまま使う場合−車を完成させて市場に出したが、市場ニ−ズで、電動ワ イパ−を取り付けたオプション仕様車も発売した。 * 既存クラスを少し修正する場合−車を完成させて市場に出したが、市場ニ−ズで、エンジ ンの一部を部品交換と追加して、タ−ボ特別仕様車も発 売した。 このようにしたい C++では ――――― ――――― A| | A| | ――――― ――――― ↓ ↓ + ――――― :…………: 点線部は A であり、 継承の機能 | A | : : によって、追加部分にプラスされ B ――――― B ――――― B になる。 |////////|←追加部分 |////////| ――――― ―――――