2日目

迷路の表示

迷路の表示は単に画面に出力する以外にも,ファイルとして出力する事も考えられます. また,ファイルの形式も,ただのテキストファイルの場合もあれば,HTMLファイルとして出力しておいてWWWブラウザで見れるようにすることも考えられます. 出力の形式は様々でも,二次元のデータを表示するという事を考えると, の4つの処理が必要となると考えられます. そこで,この4つの処理を抽象的に定義した基底となるクラスを抽象クラスとして定義し,Displayクラスと呼ぶこととします. そして,画面に出力する,ファイルに出力するなど様々な出力形態に合わせてDisplayクラスを継承したクラスを実装することで,「多態」を実現します.

Display.h

#ifndef DISPLAY_H
#define DISPLAY_H

#include <ostream>

class Display {
public:
    /**
     * デストラクタ
     */
    virtual ~Display();
    /** 
     * 表示前処理(純粋仮想関数)
     */
    virtual void start() = 0;
    /**
     * 表示後処理(純粋仮想関数)
     */
    virtual void end() = 0;
    /**
     * 1文字出力処理 (純粋仮想関数)
     * @param c 出力文字
     */
    virtual void put(char c) = 0;
    /**
     * 改行処理(純粋仮想関数)
     */
    virtual void newLine() = 0;
protected:
    /**
     * 出力先の参照(純粋仮想関数)
     * @return 出力先の参照
     */
    virtual std::ostream &stream() = 0;
};

#endif /* DISPLAY_H*/

Display.cpp

#include "Display.h"

Display::~Display() {
}
「表示前処理」はstart()により行ない,「表示後処理」はend()により行います. 「1マス出力処理」はput()により行ない,「改行処理」はnewLine()により行います. いずれのメンバ関数も先頭にvirtualと,後ろに"= 0"が付いており,純粋仮想関数となっていることに注意してください. そのため,Displayクラスは抽象クラスであり,このクラスをインスタンス化することは出来ません. Displayクラスを継承したクラスは,抽象クラスへのポインタを経由して扱うことで多態を実現することが出来,これによって,画面への表示やファイルへの出力処理を簡単に切り替えることが出来ます. また,protectedなメンバ関数stream() は出力先ストリームの参照を返すメンバ関数であり,start(),end(),put(),newLine() の処理をstream()によって返されるストリームに対して出力するように記述することで,出力先が画面であるかファイルであるかに関係なく同じコードでの記述が可能となります.
仮想関数
基本クラス内で宣言されて,派生クラス内で再定義されるメンバ関数を仮想関数と呼びます. 関数の宣言の前にvirtualというキーワードを置きます. 派生クラスで基本クラスの仮想関数を再定義する際には,関数名,仮引数の個数と型を同じにしなければなりません. 派生クラスで仮想関数の再定義を行うことを「オーバーライド」と言います. 派生クラスでオーバーライドする際に,virtualを付けても付けなくても良です. (継承されても仮想性は保たれるので,派生クラスではvirtualを付けなくても良いが,それが仮想関数であることを明確にするためにも派生クラスでもvirtualを明示的に付けておくほうが好ましいです.)
純粋仮想関数
派生クラスで必ずオーバーライドしなければならない関数のことを純粋仮想関数と呼びます. 基本クラスで
virtual type func-name(parameter-list) = 0;
のように宣言します. オーバーライドしないとコンパイルエラーになります.
抽象クラス
少なくとも1つ以上の純粋仮想関数を含んでいるクラスを抽象クラスと呼びます. 抽象クラスはインスタンスを生成することができません. 継承されて初めてインスタンス化できます,

迷路表示クラス

ここでは,迷路データを表示するクラスをMazePrinterと名付けます.

MazePrinter.h

#ifndef MAZEPRINTER_H
#define MAZEPRINTER_H

#include "Display.h"
#include "MazeMap.h"

class MazePrinter {
public:
    /**
     * コンストラクタ
     * @param display Displayインスタンスのポインタ
     */
    MazePrinter(Display *display);
    /**
     * 出力
     * @param mazeMap MazeMapインスタンスのポインタ
     */
    virtual void output(MazeMap *mazeMap);
private:
    /** Displayインスタンスのポインタ*/
    Display *display;
};

#endif /* MAZEPRINTER_H */

MazePrinter.cpp

#include "MazePrinter.h"

MazePrinter::MazePrinter(Display *display) {
    this->display = display;
}

void MazePrinter::output(MazeMap *mazeMap) {
    display->start(); // 表示前処理
    for (int y = 0; y < mazeMap->getHeight(); ++y) {
        for (int x = 0; x < mazeMap->getWidth(); ++x) {
            display->put(mazeMap->get(x, y)); // 1文字表示処理
        }
        display->newLine(); // 改行処理
    }
    display->end(); //  表示後処理
}
MazePrinterクラスのコンストラクタの役割は,Displayクラスのインスタンスへのポインタを引数として受け取り,これをメンバ変数に保存しておくことだけです. 迷路の表示はoutput()よって行われ,output()内では,start(),end(),put(),newLine()の手順のみが記載されています. この4つのメンバ関数の実際の動作はDisplayクラスを継承したクラス内で定義されることで,MazePrinterクラスを一切変更するなしに,様々な出力形態を実装することができます.

Displayクラスを継承した多態の実現

Displayクラスを継承して様々な出力形態を実現する事ができます. ここでは,以下の6種類のクラスを実装してみましょう. 横長とは横方向に2倍拡大した表示形態を指します.

DisplayScreen.h

#ifndef DISPLAYSCREEN_H
#define DISPLAYSCREEN_H

#include <iostream>
#include "Display.h"

class DisplayScreen : public Display {
public:
    virtual void start();
    virtual void end();
    virtual void put(char c);
    virtual void newLine();
protected:
    virtual std::ostream& stream();
};

#endif /* DISPLAYSCREEN_H */

DisplayScreen.cpp

#include "DisplayScreen.h"

void DisplayScreen::start() {
}

void DisplayScreen::end() {
}

void DisplayScreen::put(char c) {
    stream() << c;
}

void DisplayScreen::newLine() {
    stream() << std::endl;
}

std::ostream& DisplayScreen::stream() {
    return std::cout;
}

start(),end(),put(),newLine()は純粋仮想関数なので,start()とend()のように実装する内容がなくても必ず実装しなければなりません.
put()ではstream()で返されるストリーム(実体はstd::cout)に1文字cを挿入しているだけです.

DisplayScreenWide.h

#ifndef DISPLAYSCREENWIDE_H
#define DISPLAYSCREENWIDE_H

#include "DisplayScreen.h"

class DisplayScreenWide : public DisplayScreen {
public:
    virtual void put(char c);
};

#endif /* DISPLAYSCREENWIDE_H */

DisplayScreenWide.cpp

#include "DisplayScreenWide.h"

void DisplayScreenWide::put(char c) {
    stream() << c << c;
}

DisplayScreenクラスを継承して,put()だけをオーバーライドしています. 他のメンバ関数は明示的に記載しなくても,DisplayScreenクラスのものをそのまま流用できます.

DisplayFile.h

#ifndef DISPLAYFILE_H
#define DISPLAYFILE_H

#include <fstream>
#include "DisplayScreen.h"

class DisplayFile : public DisplayScreen {
public:
    /**
     * コンストラクタ
     * @param fileName 出力ファイル名(指定しない場合"output.txt")
     */
    DisplayFile(const char *fileName = "output.txt");

    virtual ~DisplayFile();
protected:
    virtual std::ostream& stream();
private:
    std::ofstream ofs;
};

#endif /* DISPLAYFILE_H */

DisplayFile.cpp

#include "DisplayFile.h"

DisplayFile::DisplayFile(const char *fileName) {
    /* ここを実装してください */
}

DisplayFile::~DisplayFile() {
    /* ここを実装してください */
}

std::ostream& DisplayFile::stream() {
    return ofs;
}


コンストラクタのディフォルト引数でファイル名を指定しています.
ファイルに出力するために,stream()をオーバーライドして,ストリームとしてofstreamを返すようにしています.

DisplayFileWide.h

#ifndef DISPLAYFILEWIDE_H
#define DISPLAYFILEWIDE_H

#include "DisplayFile.h"

class DisplayFileWide : public DisplayFile {
public:  
    virtual void put(char c);
};

#endif /* DISPLAYFILEWIDE_H */

DisplayFileWide.cpp

#include "DisplayFileWide.h"

void DisplayFileWide::put(char c) {
    /* ここを実装してください */
}
DisplayScreenWideクラスの実装と考え方は全く一緒です.

DisplayHtml.h

#ifndef DISPLAYHTML_H
#define DISPLAYHTML_H

#include "DisplayFile.h"

class DisplayHtml : public DisplayFile {
public:
    /**
     * コンストラクタ
     * @param fileName 出力ファイル名(指定しない場合"output.html")
     */
    DisplayHtml(const char *fileName = "output.html");

    virtual void start();
    virtual void end();
    virtual void put(char c);
    virtual void newLine();
};

#endif /* DISPLAYHTML_H */

DisplayHtml.cpp

#include "DisplayHtml.h"

DisplayHtml::DisplayHtml(const char* fileName) : DisplayFile(fileName) {
}

void DisplayHtml::start() {
    stream() << "<html><body>\n<table>\n<tr>\n";
}

void DisplayHtml::end() {
    stream() << "</tr>\n</table>\n</body>\n</html>\n" << std::endl;
}

void DisplayHtml::put(char c) {
    stream() << "<td>" << c << "</td>";
}

void DisplayHtml::newLine() {
    stream() << "</tr>\n<tr>\n" ;
}




start(),end(),put(),newLine()をHTML出力用にオーバーライドしています.

DisplayHtmlWide.h

#ifndef DISPLAYHTMLWIDE_H
#define DISPLAYHTMLWIDE_H

#include "DisplayHtml.h"

class DisplayHtmlWide : public DisplayHtml {
public:
    virtual void put(char c);
};

#endif /* DISPLAYHTMLWIDE_H */

DisplayHtmlWide.cpp

#include "DisplayHtmlWide.h"

void DisplayHtmlWide::put(char c) {
    stream() << "<td>" << c << c << "</td>";
}
DisplayScreenWideクラスの実装と考え方は全く一緒です.
DisplayFile.cppとDisplayFileWide.cppの未実装の3つのメンバ関数を実装しましょう.

各クラスの継承関係

これらのクラスの継承関係は下図のようになります.

基本クラスとしてDisplayクラス(抽象クラス)があり,それを継承して画面への表示機能を実現したDisplayScreenクラスがあります. ファイルへの出力機能を実現したDisplayFileクラスはDisplayScreen クラスを継承することで,1マス文字出力と改行処理の記述を再利用しています. 両者の違いは出力先であるファイルのオープンとクローズ処理のみです. HTMLファイルへの出力機能を実現したDisplayHtmlクラスはDisplayFile クラスを継承することで,ファイルのオープンとクローズ処理の記述を再利用しています. DisplayScreenWideクラスなどの横長出力を実現したクラスはDisplayScreenクラスなどのそれぞれ対応する基本クラスを継承して1マス出力機能部分を記述を変更するだけで実現されています.

迷路作成アプリケーションの実装

ここまでの学習で,迷路を自動的に作成し表示するアプリケーションのための3つのクラスが完成しました. また,表示のためのDisplayクラスを継承した6つのクラスが完成しました. そこで,完成した迷路作成アプリケーションの動作を以下のテストドライバを用いて確認してみましょう.

main.cpp

#include <cstdlib>
#include "MazeMap.h"
#include "MazeMakerSimple.h"
#include "MazePrinter.h"
#include "Display.h"
#include "DisplayScreen.h"
#include "DisplayScreenWide.h"
#include "DisplayFile.h"
#include "DisplayFileWide.h"
#include "DisplayHtml.h"
#include "DisplayHtmlWide.h"

/**
 * 表示方法の選択
 * @return Displayインスタンスのポインタ
 * @attention Displayインスタンスを生成しているので,不要になった時点でdeleteすること
 */
Display *selectDisplay() {
    int outputMode;
    Display *display = NULL;
    do {
        std::cout << "output mode [1-6] ? ";
        std::cin >> outputMode;
        switch (outputMode) {
             case 1: display = new DisplayScreen; // 画面に出力
                break;
            case 2: display = new DisplayFile; // ファイルに出力
                break;
            case 3: display = new DisplayHtml; // HTMLファイルに出力
                break;
            case 4: display = new DisplayScreenWide; // 画面に横長に出力
                break;
            case 5: display = new DisplayFileWide; // ファイルに横長に出力
                break;
            case 6: display = new DisplayHtmlWide; // HTMLファイルに横長に出力
                break;
            default:
                std::cout << "illegal output mode " << outputMode << std::endl;
                break;
        }
    } while (display == NULL);
    return display;
}

int main(int argc, char **argv) {
    // 乱数の初期化設定
    // 本来は実行する度に変化する時刻情報などを用いたほうが良いが,開発を容易に(実行の度に結果が変わらないように)するために,乱数の初期値を手動で設定し固定する
    int seed;
    std::cout << "seed ? ";
    std::cin >> seed;
    srand(seed); // 乱数種の設定

    // 迷路幅,迷路高の基底の設定
    int w, h;
    std::cout << "size (w, h) ? ";
    std::cin >> w >> h;

    // 迷路マップ情報生成
    MazeMap mazeMap(w, h);

    // 迷路作成
    MazeMakerSimple mazeMaker(&mazeMap);
    mazeMaker.generate();

    // 迷路出力
    Display *display = selectDisplay();
    MazePrinter mazePrinter(display);
    mazePrinter.output(&mazeMap);

    // selectDisplay()内で生成したインスタンスを解放
    delete display;

    return 0;
}

出力例

seed ? 172900
size (w, h) ? 20 10
output mode [1-6] ? 4
OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
SS              OO                  OO              OO          OO          OO  OO
OO  OOOOOO  OOOOOO  OOOOOOOOOO  OO  OOOOOO  OOOOOO  OOOOOO  OO  OO  OOOOOO  OO  OO
OO  OO  OO      OO      OO      OO          OO      OO      OO      OO      OO  OO
OO  OO  OO  OO  OOOOOO  OO  OOOOOOOOOOOOOOOOOO  OOOOOO  OOOOOOOOOOOOOOOOOO  OO  OO
OO  OO  OO  OO          OO          OO  OO      OO      OO              OO      OO
OO  OO  OO  OOOOOOOOOOOOOOOOOOOOOO  OO  OO  OOOOOOOOOO  OO  OOOOOOOOOO  OOOOOOOOOO
OO  OO  OO  OO          OO      OO  OO  OO          OO              OO  OO      OO
OOOOOO  OO  OOOOOO  OO  OO  OOOOOO  OO  OOOOOOOOOO  OOOOOOOOOOOOOOOOOO  OO  OO  OO
OO      OO          OO      OO      OO  OO      OO  OO      OO          OO  OO  OO
OO  OOOOOOOOOO  OOOOOOOOOOOOOO  OOOOOO  OO  OO  OO  OO  OO  OO  OOOOOOOOOO  OO  OO
OO          OO  OO              OO      OO  OO  OO      OO  OO              OO  OO
OOOOOOOOOO  OOOOOO  OOOOOOOOOOOOOO  OO  OO  OO  OOOOOOOOOO  OO  OOOOOOOOOO  OO  OO
OO      OO      OO      OO          OO  OO  OO          OO  OO  OO      OO  OO  OO
OO  OOOOOOOOOO  OO  OO  OO  OOOOOOOOOO  OO  OOOOOOOOOO  OO  OOOOOO  OO  OOOOOO  OO
OO  OO      OO  OO  OO  OO  OO          OO      OO  OO      OO      OO          OO
OO  OO  OO  OO  OOOOOO  OO  OOOOOOOOOOOOOOOOOO  OO  OOOOOOOOOO  OOOOOOOOOOOOOO  OO
OO      OO      OO      OO      OO      OO      OO      OO                  OO  OO
OO  OOOOOOOOOOOOOO  OOOOOO  OO  OO  OO  OO  OOOOOO  OO  OOOOOOOOOOOOOOOOOO  OO  OO
OO                  OO      OO      OO              OO                      OO  GG
OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
続行するには何かキーを押してください . . .