ObjectSquare [2002 年 8 月号]

[技術講座]


旧新人による新人向けオブジェクト指向入門

- C++ でオブジェクトを動かしてみよう -

株式会社 オージス総研
オブジェクトテクノロジソリューション部
池上 裕樹


はじめに

新人の皆さんこんにちは。入社式の季節から約4ヶ月がたち、いよいよ実務に入ろうかという時期かと思います。これまでの研修を経て、コンピュータの仕組みや、プログラミング、方法論などさまざまなことに興味をもたれたのではないかと思います。中には、オブジェクト指向に興味をもたれた方もおられるかと思います。私もいまから1年ほど前にこの世界に入り、オブジェクト指向という考え方に触れたわけですが、オブジェクト指向の良さ、難しさなど身をもって体験してきました。最近では、オブジェクト指向による開発が一般的になりつつあると思いますが、これから実務でオブジェクト指向による開発をされる新人の皆さんに対して、オブジェクト指向に少しでもなじんでいただこと思い、記事を書いてみました。

この記事では、”オブジェクト”についてさまざまな切り口から見ていきます。また、プログラミング、オブジェクト指向の初心者の方を対象にしています。言語は C++ を使用します。拙い文章ですが、最後までお付き合いください。

1. オブジェクトとは何か

まずオブジェクトとは、何かについて考えてみましょう。Grady Booch の定義では、オブジェクトは、状態、振る舞い、識別性を持った"もの"です。 [参考文献 1]

状態 そのオブジェクトが内部に持っているデータのことです。
振る舞い そのオブジェクトの動作や反応のことです。
識別性 あるオブジェクトと別のオブジェクトの実体が異なっていることを識別できることです。

では、具体的に、どのような "もの" がオブジェクトか考えてみましょう。ここでは、野球をとりあげてみます。

まず、すぐに思い浮かぶのは、現実に存在する"もの"です。例えば、"キヨハラ"選手 は、打つ、守るなど振る舞いを持つでしょう。そして、打率 .300、本塁打数 50、などの状態を持つと考えられます。そして、”キヨハラ”選手は、”マツイ”選手と違うと判別できます。つまり、それぞれ識別することも可能です。

また、仮想的な"もの"もオブジェクトになる場合があります。例として、"ジャイアンツチーム"について考えてみます。”ジャイアンツチーム”は物理的に存在するものではありませんが、攻撃するという振る舞いと、守るという振る舞いが考えれます。また、勝ち数、負け数、引き分けなどの状態を持ちます。また”ジャイアンツチーム”は、やはり”タイガースチーム”と、識別することが可能です。

そして、概念的な "こと" をオブジェクトとして扱うこともあります。例えば、"ルール"というのはどうでしょう。 野球のルールには、”公認野球規則”というものがあります。”公認野球規則”とは、アメリカのルール委員会が毎年更新・決定する野球の基本です。そして、このルールをもとにそれぞれのリーグによって、独自のルールを作っています。さて、これはオブジェクトといえるでしょうか?
まず、振る舞いを考えてみましょう。ルールの振る舞いとして考えられるのは、3 アウトになったら攻撃と守備を入れ替えるや、9 イニング終了するとゲームセットとするといったものが考えられそうです。また、状態としては、アウト数、イニング数、ストライク数などになるでしょうか。そして、セ・リーグのルールとパ・リーグのルール、高校野球のルールは、それぞれことなっており、また識別可能です。どうやら、ルールもオブジェクトとなりえそうです。

仮想的な"もの"や、概念的な "こと" をオブジェクトとして扱うことに違和感を感じる方が多いと思います。この辺りについては、モデリングの勘所 -オブジェクトモデルと実世界- で非常に面白い話を読むことができます。

オブジェクト指向による開発では、オブジェクトを生成し、そのオブジェクト同士がメッセージを送り協調動作することによって、システムを作り上げていくことになります。では、”ジャイアンツチーム”対”タイガースチーム”の試合のシステムを考えて見ましょう。まず、オブジェクトとして、出てくるものをあげてみましょう。”キヨハラ”選手、”マツイ”選手、”イガワ”選手・・・。切りがありませんので、この辺で止めますが、少なくとも 18 のオブジェクト(選手だけでも 18 人いるのですから!! )が必要になってきそうです。これらを 1 つ 1 つ考えていくのは、ちょっと煩雑になりそうです。

では、どうしましょうか? よくよく考えると”キヨハラ”選手、”マツイ”選手、”イガワ”選手には共通性がありそうです。例えば、打つ、守るなどの振る舞いや、打率を求めるための打席数や安打数、本塁打数などの状態はみんな持っているはずです。これらの共通項をまとめて考えたほうが、わかりやすそうですね。これらオブジェクトの共通の部分を定義したものをオブジェクト指向の世界では、クラスと呼びます。ここでは、打つ、守るなどの振る舞いや、打席数、安打数、本塁打数などの状態を持つ選手というクラスを誕生させましょう。

これらクラスを発見することは、実は非常に難しいことです。また、たとえクラスとしての性質を持っていたとしても、対象とする問題領域(ドメイン) [この場合は野球の試合] によって、クラスとして扱わない場合もあります。あるドメインのクラスを見つけるということは、多くの経験と知識を必要とするため、非常に難しいことです。皆さんも、是非いろいろなドメインで、どのようなものがクラスになるか考えてみてください。

これらクラスの抽出と、そのクラスが妥当かどうかを考える際のヒントとして、2つの方法を紹介します。

1つ目が名詞抽出法です。これは、対象となるドメインについての矛盾なく、簡潔な文章から、名詞または名詞句をとりあげます。すなわち、そのドメインが興味をもっている”もの”を抽出することであるといえます。

2つ目がCRC カードを用いた方法です。CRC カードとは、クラス(Classes)、責務(Responsibilities)、コラボレーション(Collaborations)の頭文字を集めたものですが、それぞれを以下のような小さなカードに書き込んでいきます。クラス名にはそのクラスにふさわしい名前を、リスポンシビリティはそのクラスが持つ責務(オブジェクトが責任を持って行わなければならない振る舞い)を、コラボレーションには、そのクラスの責務を手助けするほかのクラスを書き込むことになります。CRC カードを使う利点は、小さなカードを用いることによって、1 つのクラスが責務を持ちすぎることを抑制することができる点です。また、実世界のオブジェクトでないクラス間の関係を識別するのに役立ちます。[参考文献 1]

責務についてはオブジェクト指向設計帖で詳細な解説を読むことができます。

さて、18個もあったオブジェクトを1 つのクラスに定義することによってすっきりしたのはよいのですが、今度はなんだか漠然としていてイメージがわきにくくなってきました。おまけに、この選手クラスがルールやチームとどのような関係にあるのかいまいちピンときません。なにか目に見える形で表現したくなってきました。

目に見える形で表現するといっても、これをいちいち自分で考えるのはなんだかめんどくさいですね。しかも美的センスのない私が作ると余計にわかりにくくなりそうです。ではどうしましょうか? 実はこれらの要望に応えることのできる表現方法は、すでに用意されています。それが UML (Unified Modeling Language) です。これを使わない手はありません。是非とも先人たちの知恵を借りましょう。では、先ほどあげたクラスを UML のクラス図(システムの静的な関係を表現する図)で表現してみましょう。クラス図では、クラスは図 1 のように表現されます。上から、クラス名、状態、振る舞いを記述していくことになります。

//## 図 1 (UML によるクラスの表記)

UML によるクラスの表記例1

これなら、一目瞭然です。この一目瞭然な表現というのは、実際の開発において非常に有効です。

第一にコミュニケーションがとりやすくなる点です。開発は通常複数のメンバによって行われますが、それぞれの開発者が共通の表現を用いることによって、円滑にコミュニケーションをとることができるようになります。

第二に、あいまいさがなくなる点です。これによって、システム全体を形式的表現できるようになり、システムの問題箇所を容易に発見できるようになります。

第三に、先人たちの知恵を借りることができるようになります。例えば、今までに幾度となく同じような開発が世の中のあちこちでされているわけですが、それらの開発によって考え出されたきまりきった形(パターン)を模倣することによって、早く、簡単に、開発を進めることができます。また良いシステムを作ることができるようになります。これらの例として、アナリシスパターンデザインパターンなどが有名です。[参考文献 4]

2. プログラミング言語に見るオブジェクト

一目瞭然のクラスを表現できてすっきりしましたが、それだけ見ていても面白くありません。微動だにしない”キヨハラ”選手よりスカッとするホームランを打ってくれる”キヨハラ”選手をみたいのが人情というものです。では、実際に動く”キヨハラ”選手を作ってみましょう。プログラミング言語は C++ を使います。(ここで詳細なプログラミング言語の解説はいたしません。お手持ちの C++の解説書を片手に眺めてみてください。)

次へ進む前に先ほど作った UML で書いたクラスもプログラミング言語に合うように変更しておきましょう。

//## 図 2 (UML によるクラスの表記)

UML によるクラスの表記例2

まず、プログラミング言語で用いることができるように、英語表記にしました。他にも図 1 と比べて、情報が多くなっています。まず、+ や - といった記号がつきました。さらに、 ( ) や : 以下に基本型情報が入りました。これらについて、順番に紹介したいと思います。

C++ では、1つのクラスに対して、ヘッダーファイル(.h) とソースファイル(.cpp) の 2 つのファイルを作ります。ヘッダーファイルには、クラスの型情報を書きます。ソースファイルには、クラスの持つ振る舞いを書きます。C++ では、振る舞いをメンバ関数、状態をメンバ変数と呼びます。

ヘッダーファイルから見ていきましょう。

//## Player クラスのヘッダーファイル
// Player.h 

#include  <string>
using namespace std;

enum swing { STRIKEOUT, HIT, HOMERUN };

class Player 
{
  public:
    //## コンストラクタ
      Player();

    //## デストラクタ
      virtual ~Player();

    //## メンバ関数
      void setName (string myName);
      const string getName () const;
      swing hit ();
      void displayRecord ();

  private:
    //## メンバ変数
      string name;
      float plateAppearance;
      float hitCount;
      int homeRun;
};

 

コンストラクタ / デストラクタ

コンストラクタは、クラスからオブジェクトを生成する際に、システムに自動的に呼ばれる特殊なメンバ関数です。コンストラクタは、基本的にデータメンバを初期化するための関数といえます。 デストラクタは、オブジェクトが解放されるときにシステムによって自動的に呼ばれる特殊なメンバ関数です。C++ では、コンストラクタとデストラクタはいくつかのルールがあります。

  1. クラスと同じ名前で書く(デストラクタは ~ がつく)。
  2. 戻り値は書くことができない。
  3. デストラクタは、引数も渡せない。

戻り値 / 引数

メンバ関数に、void 、int 、string などがついています。これは、この関数を実行すると、この基本型の値が戻されることを表しています。int は数値を、string は文字列を返します。void は、戻り値がないことを表します。

setName 関数の() の中に、string myName がついています。これが引数です。引数は、関数の呼び出し側と関数との間で,情報の受け渡しに使用される変数や定数のことです。setName 関数を呼び出す場合は、string 型の引数が必ず必要になります。myName は、渡された引数をこの関数で扱う際の擬似的な名前になります。

アクセス指定子

アクセス指定子は、可視性を表現するものです。可視性とは、ほかのクラスから見ることができるかどうかを表現します。可視性には、すべてのクラスから見ることができる public (UML 表記では +)、そのクラス内でしか見ることのできない private(UML 表記では -)などがあります。C++ では、その他にも protected や friend といったアクセス指定子が用意されています。通常、状態は private、振る舞いは public とします。これは、オブジェクトが持つ状態は、そのオブジェクトが持つ振る舞いによってのみ操作可能が可能にするためです。これをカプセル化といいます。カプセル化をすることによって、そのオブジェクトの独立性が高まることとなります。

using namespace std; とかかれている部分は、とりあえずおまじないとして記述する必要があるということを覚えておきましょう。

次に、ソースファイルを見てみましょう。

//## Player クラスのソースファイル
// Player.cpp

#include "Player.h"
#include <cstdlib>
#include <ctime>
#include <iostream>

using namespace std;

//## コンストラクタ
Player::Player()
	// メンバ変数の初期化を行う
      : plateAppearance(0),
        hitCount(0),
        homeRun(0)
{
        // 実行するたびに違う値が得られるように、現在の時刻値を使って
	// 乱数ジェネレータを初期化する。
 	srand((unsigned)time( NULL ) );
}

//## デストラクタ
Player::~Player()
{
}

//## メンバ関数
void Player::setName (string myName)
{
	name = myName;
}

const string Player::getName () const
{
	return name;
}

swing Player::hit ()
{

	// 乱数から値を取得する
  	swing result = static_cast<swing>(rand() % 3);
	
	// 打席数に1を足す。
	plateAppearance++;

 	switch (result) {

	// STRIKEOUT だったら空振り
 	case STRIKEOUT:
		break;

	// HIT だったらヒット
	case HIT:
		// ヒット数に1を足す。
		hitCount++;
		break;

	// HOMERUN だったらホームラン
	case HOMERUN:
		// ヒット数、ホームラン数に1を足す。
		hitCount++;
		homeRun++;
		break;
	}
	return result;
}

void Player::displayRecord ()
{
	// 打率を計算する
	float average = (hitCount / plateAppearance) * 1000;

	// 打率を表示する
	cout << "打率:" << average << endl;

	// 本塁打数を表示する
	cout << "本塁打数:" << homeRun << endl;
}

ソースファイルには、メンバ関数の具体的な振る舞いが記述されています。例えば、setName 関数は、引数で渡された myName をメンバ変数 name に代入しています。 また、getName 関数はメンバ変数 name を戻り値としてこの関数の呼び出し元に返しています。 hit 関数では、ランダムな値をもとに、選手にバットを振る処理を書いています。displayRecord 関数では、結果をそれぞれ表示する処理を書いています。

ここで注意していただきたいのは、メンバ変数の初期化の方法です。さきほど、コンストラクタは、基本的にデータメンバを初期化するための関数であると書きましたが、実際には関数内部ではなく、上記のようにコンストラクタの関数名のあとに「:」を書き、そのあとにメンバ名と初期化値を記述します。これは、定数のメンバに対応するためです。C++ には、関数内部では定数を書き換えることができないという制約があります。コンストラクタも特別とはいえ関数ですので、定数の初期化を行うことができないということになります。そこで、上記のように記述し、定数に値を設定することになります。C++ では、「コンストラクタ内の代入」と「初期化」は明確に区別されていますので、定数に限らず通常のメンバについても、初期化の際には上記の記述をしたほうがよいでしょう。 [参考文献 6]

では次章以降から、上記のクラスからオブジェクトを生成し動かしてみましょう。

3. プログラム実行までの流れ

話は少し変わりますが、ここでプログラムの実行がどのように行われているのかを整理しましょう。

現在使われているコンピュータのほとんどのアーキテクチャは、ノイマン型コンピュータです。ノイマン型コンピュータは、プログラム内蔵方式 [プログラムとデータを主記憶装置(メモリ)に一端格納してから実行する]、逐次制御方式 [プログラムに書かれた命令を、一つ一つ実行する] を大きな特徴にしています。つまり、プログラムを実行するとは、メモリにロードされたマシン語のプログラムが、CPU によって解釈・実行されシステム制御や演算が行われるということです。 この仕組みは半世紀も前に考え出されましたが、ほとんどのコンピュータがいまだこの仕組みで動いています。オブジェクト指向といっても、この部分についてなんら変わるところないということをおさえておきましょう。

では、C++ のプログラムを実行するまでの過程を見てみましょう (図3 参照)。まず、C++ で記述されたプログラムを書き、ヘッダーファイルと、ソースファイルを作成します。次に、ソースファイルをコンパイルし、オブジェクトファイルを生成します。これは、ソースファイルをコンピュータが理解できるネイティブコードに翻訳するということです。 そして、ライブラリ(複数のオブジェクトファイルをまとめたもの)をリンクします。 そして、実行ファイルが生成されます。

プログラム実行過程
//## 図3 プログラムを実行するまでの過程

それでは、いままで作成したオブジェクトを動かしてみましょう。MS-DOS プロンプト上などで実行するプログラムでは、main という名前の関数が実行開始位置(エントリポイント)になります。今回は、”キヨハラ”選手に 10 球ボールを投げて、バッティングしてもらいます。

オブジェクトに対して、何かを行ってもらうためには、そのオブジェクトに対して、メッセージを与えてやることになります。以下のコードでは、Player クラスから生成したオブジェクトに"キヨハラ"という名前をつけるというメッセージを送り、その後 バッティングをするというメッセージを 10 回を送り、結果を表示しろというメッセージを送っています。

ここで取り上げたコードは実行可能ですので、一度試してみてください。現在手元に、C++ コンパイラをお持ちでない方は、ボーランド社から C++ Compiler が無償提供されていますので、そちらをご活用ください。

//## main 関数
#include <iostream>
#include "Player.h"

using namespace std;

void main (int argc, char * argv[]) {
	// ヒープ領域にPlayerクラスから、Playerを作る。
	Player* player = new Player();

	// Player に "kiyohara" という名前を付ける。
	player->setName("キヨハラ");

	// 10 回トスバティングを行う。
	for (int i = 0; i < 10; i++) {
		swing result = player->hit();

	// 結果を表示する。
		switch (result) {
		case STRIKEOUT:
			cout << "空振り" << endl;
			break;
		case HIT:
			cout << "ヒット" << endl;
			break;
		case HOMERUN:
			cout << "ホームラン" << endl;
			break;
		}
	}

	// 結果(打率と本塁打数)を表示する。
	player->displayRecord();

	// バッターボックスからでる。
	delete player;
}

 

4. オブジェクトの生成と削除

プログラムが実行されている間のオブジェクトはどのようになっているのでしょうか。先ほども述べたように、プログラムを実行するとは、メモリにロードされたマシン語のプログラムが、CPU によって解釈・実行されシステム制御や演算が行われるということです。オブジェクトもプログラムですから、当然メモリにロードされる必要があります。では、メモリとオブジェクトについて見ていきましょう。メモリの領域を確保には、以下の3つの方法があります。

静的メモリ獲得と、動的メモリ獲得に大別され、さらに動的メモリ獲得には、スタックベースの獲得と、ヒープベースの獲得の2つがあります。

静的メモリ獲得は、外部変数や外部関数等があります。また、static 宣言された変数や、関数も静的メモリ獲得を行います。静的メモリ獲得は、コンパイル時、ライブラリがロードされる時、またプログラムの実行が開始される時にメモリ獲得が行われます。プログラムが実行されている間は、消滅しません。

動的メモリ獲得では,プログラムの実行途中にメモリの確保が行われます。この動的メモリ獲得は,さらに,スタックベースとヒープベースの動的メモリ獲得に分けられます。

スタックとは、C++ では関数の内部で一時的に使われる変数(内部変数)や、関数を呼び出すときのパラメータを格納するためのメモリ領域です。スタックベースのメモリ獲得は、後入れ先出し (LIFO) を用いて実現されます。 スタックの領域は、一時的な領域ですので、処理がオブジェクトのスコープから外れると、その領域は開放されるため、プログラマは、オブジェクトの解放を意識する必要はありません。

ヒープとは、プログラムの実行時に動的に作成される配列やオブジェクトを格納するためのメモリ領域です。具体的には、オブジェクトを生成する new 演算子で確保し、delete 演算子で開放します。また、alloc 演算子, free 演算子等といったオブジェクトを生成せずに領域を確保する方法もあります。new演算子 / delete 演算子と malloc 演算子 / free 演算子の違いは、コンストラクタ、デストラクタが働くかどうかの違いです。この領域は、スタックとは違い、プログラマの責任で領域を確保、または開放する必要があります。この開放を忘れると、メモリリークという非常に怖いバグが発生します。このバグの怖さは、メモリが蓄積されオーバーフローを起こすまで、異常な動作をしないため、非常に気付きにくい点にあります。必ず、new と delete は対応付けて書くようにしましょう。

さて、2章の最後であげたオブジェクトの生成の 2 つの方法と照らし合わせて考えて見ます。それぞれのオブジェクトは、以下のように宣言する場所、宣言の方法によって作り分けることができます。

//## さまざまなオブジェクトの生成 と削除

Player player; // 関数外部で宣言する場合(静的メモリ獲得)

void func ( ) {

Person player; // 関数内部で宣言する場合(スタックベースの動的メモリ獲得)

}

void func ( ) {

Player* player = new Player( ); // 関数内部で宣言する場合(ヒープベースの動的メモリ獲得)

delete player; // かならず削除する必要がある。

}

最後に

いかがだったでしょうか? 今回は、オブジェクトとクラスの概念、C++ 言語での表現、オブジェクトの動かし方などについてみていきました。今回取り上げた内容は、非常に初歩的な内容ですので、これを機会に、さまざまな書籍に目を通して勉強に励んでください。以下にあげた参考文献は、いずれも良書であると思いますので、ぜひご一読ください。また、次のステップとしてオブジェクトの広場で取り上げてきた内容にもぜひ目を通していただきたいと思い、以下にリンクを貼っておきます。この記事が、新人の方のオブジェクト指向への理解の一助になれば幸いです。

UML について深く知りたい方へ オブジェクト指向モデリング言語 UML

オブジェクト指向による分析 / 設計に興味のある方へ

オブジェクト指向設計帖
モデリングの勘所 -オブジェクトモデルと実世界-
オブジェクト指向関連の図書について詳しく知りたい方へ オブジェクト指向を学ぶための入門書ガイド

 


参考文献

  1. 「オブジェクト指向とコンポーネントによるソフトウェア工学 - UMLを使って -」
                 ペルディタ・スティーブンス / ロブ・プーリー 著 (ピアソン・エデュケーション)
  2. 「UMLモデリングのエッセンス 第2版」マーティン・ファウラー / ケンドール・スコット 著 (翔泳社)
  3. 「独習 C++」 ハーバート・シルト 著 (翔泳社)
  4. 「かんたん UML」(株)オージス総研 著 (翔泳社)
  5. 「10日でおぼえるUML入門教室」(株)オージス総研 著 (翔泳社)
  6. 「憂鬱なプログラマのためのオブジェクト指向開発講座」 Tucker! 著 (翔泳社)
  7. 「オブジェクト指向入門」バートランド・メイヤー 著 (アスキー出版局)
  8. 「やさしいUML入門」 浅見 智晴 著 (ピアソン・エデュケーション)


記事の内容を5点満点で評価してください。
1点 2点 3点 4点 5点
記事に関するコメントがあれば併せてご記入ください。
  

© 2002 OGIS-RI Co., Ltd.
HOME HOME TOP オブジェクトの広場 TOP