第18章 ピュア

 CFile はバイナリモードでファイルを扱うクラスでした。そして、それから派生して CTextFile というテキストモードでファイルを扱うクラスを作りました。今回はこの派生方法を見直してみたいと思います。


 今回の要点は以下の通りです。


 では、いってみましょう。


 CFile はバイナリモードでファイルを扱うクラスです。そして、CTextFile はテキストモードでファイルを扱うクラスです。そして、CTextFile は CFile から派生しています。

 図にするとこんな感じです。

CFile
CTextFile

 しかし、このままではバイナリモードで扱うときの独自の機能を付けたいときでも、その機能は CTextFile にも継承されてしまいます。これは困ります。

 ということで、バイナリモードでファイルを扱うクラス CBinaryFile クラスを別に作ることにします。

 しかし、ファイルを扱うクラスなのでやはり CFile クラスから派生したいと思います。こういう継承をするわけです。

CFile
/      \
CBinaryFile CTextFile

 こうすると、CBinaryFile にメンバを追加しても CTextFile に影響はありませんね。

 では、実際にやってみましょう。

プログラム1
// BinFile.h
#ifndef __BINFILE_H__INCLUDED__
#define __BINFILE_H__INCLUDED__

#include "File.h"

class CBinaryFile : public CFile
{
    // コンストラクタ
public:
    CBinaryFile();
    CBinaryFile(const char* pszPath, const char* pszFlags);
    CBinaryFile(const CBinaryFile& rother);

    // ファイルの読み書き
public:
    int ReadAndDump(char* bufHex, char* bufASCII, int nLength);
            // ダンプ文字列にして読み出し
};

#endif
プログラム2
// BinFile.cpp
#include <stdio.h>
#include <memory.h>
#include <ctype.h>
#include "BinFile.h"

// デフォルトコンストラクタ
CBinaryFile::CBinaryFile()
{
}

// 構築と同時にファイルを開く
CBinaryFile::CBinaryFile(const char* pszPath, const char* pszFlags)
{
    Open(pszPath, pszFlags);
}

// コピーコンストラクタ
CBinaryFile::CBinaryFile(const CBinaryFile& rother) : CFile(rother)
{
}

// ダンプ文字列にして読み出し
// ダンプについては第1部第55章第3部第15章を参照して下さい
// bufHex のサイズは nLength * 3 + 1
// bufASCII のサイズは nLength + 1 にします
int CBinaryFile::ReadAndDump(char* bufHex, char* bufASCII, int nLength)
{
    int nRead;  // 読み出しバイト数
    int i;      // ループ変数

    // 読み込み
    nRead = Read(bufASCII, nLength);
    memset(bufASCII + nRead, ' ', nLength - nRead);
    bufASCII[nLength] = 0;

    // 16進表示の文字列を作成
    for(i = 0; i < nRead; i++)
    {
        // sprintf については第1部第24章を参照して下さい
        // unsigned char でキャストしていることについては
        // 第3部第16章を参照して下さい
        sprintf(&bufHex[i * 3], "%02X ", (unsigned char)bufASCII[i]);
    }
    memset(&bufHex[i * 3], ' ', (nLength - nRead) * 3);
    bufHex[nLength * 3] = 0;

    // 表示できない文字を '.' に変換
    for(i = 0; i < nRead; i++)
    {
        // isprint は表示できる文字かを確認する関数です
        // 全角文字は変になりますが、無視します
        // unsigned char でキャストしないと
        // 0x80 〜 0xFF がマイナスになってしまいます
        // 必要なヘッダファイルは ctype.h です
        if(isprint((unsigned char)bufASCII[i]) == 0)
            bufASCII[i] = '.';
    }

    return nRead;
}
プログラム3
// TestFile.cpp
#include <iostream.h>
#include "BinFile.h"
#include "TextFile.h"

int main()
{
    CTextFile   txt("Test.txt", "w");
    if(txt.IsValid() == false)
        return 0;

    txt.WriteString("He sang, \"まったり まったり まったりな〜♪\"");
    txt.Close();

    CBinaryFile bin("Test.txt", "r");
    if(bin.IsValid() == false)
        return 0;

    const int READLENGTH = 16;
    char bufHex[READLENGTH * 3 + 1];
    char bufASCII[READLENGTH + 1];
    int nRead;

    do
    {
        nRead = bin.ReadAndDump(bufHex, bufASCII, READLENGTH);
        cout << bufHex << bufASCII << endl;
    }
    while(nRead == READLENGTH);

    return 0;
}
実行結果
CFile::Open
CTextFile::ModifyFlags
CFile::Open
CFile::ModifyFlags
48 65 20 73 61 6E 67 2C 20 22 82 DC 82 C1 82 BD He sang, "......
82 E8 81 40 82 DC 82 C1 82 BD 82 E8 81 40 82 DC ...@.........@..
82 C1 82 BD 82 E8 82 C8 81 60 81 F4 22          .........`.."

 こうして、バイナリモードでファイルを扱うクラス CBinaryFile が作れました。


 しかし、こうなると ModifyFlags 関数は CFile には不要です。

 CFile はただのファイルを扱うクラスに成り下がってしまい、「バイナリモードで」ということをわざわざ CFile で実装する必要はありません。CFile の ModifyFlags でやっている処理は、CBinaryFile クラスで実装した方が良さそうです。

 そうすると、CFile の ModifyFlags は不要になってしまいますね。ところが、仮想関数を使うためには残しておく必要があります。完全になくしてしまっては CFile の Open から呼び出すことが出来なくなります。

 ということで、中身をほとんど消して定義しておきました。

// File.cpp
// フラグの調整
bool CFile::ModifyFlags(const char* pszPath, const char* pszFlags)
{
    return false;  // 常に失敗
}
// BinFile.h
class CBinaryFile : public CFile
{
    // フラグの調整
private:
    virtual bool ModifyFlags(const char* pszPath, const char* pszFlags);
};
// BinFile.cpp
#include <iostream.h>
#include <string.h>

// フラグの調整
bool CBinaryFile::ModifyFlags(const char* pszPath, const char* pszFlags)
{
    // 以前に CFile で実装していた中身をここにコピー
    // ただし、cout の部分の文字列を CBinaryFile::ModifyFlags に変えること
}

 こうして、意味のある実装が出来ました。


 そして、更に突き詰めて考えると CFile 自体を直接使うこともありません。バイナリモードでは CBinaryFile を、テキストモードでは CTextFile を使えばいいのですから、CFile を使うことはなくなります。

 このように、実体は作らないが、基底クラスとして必要となるクラスというのもたまにあります。こういうときは、上の CFile::ModifyFlags を次のようにすることが出来ます。

// File.h
class CFile
{
    // フラグの調整(純粋仮想関数)
private:
    virtual bool ModifyFlags(const char* pszPath, const char* pszFlags) = 0;
};

 何と、クラス宣言内の ModifyFlags に0が代入されています! そして、何と、この関数の実装はどこでも行いません!

 不気味な書式に感じるとは思いますが、こうすれば実装する必要のない仮想関数実装をする必要がなくなります。その代わり、こういう関数を持つクラスの実体は作れません。純粋仮想関数を呼ぶ処理が書けないからですね。

 このような仮想関数を純粋仮想関数と、そして純粋仮想関数を持つクラスを抽象クラスと呼びます。

 純粋仮想関数をオーバーライドし忘れていると、そのクラスまで抽象クラスになってしまいます。そこは注意して下さいね。


 では、最後にファイルを扱うクラスの役割をまとめてみましょう。

CFile(抽象クラス) ファイルを扱うクラスの最基底クラス。ファイルを扱うクラスに共通する処理をここに書く。
 ├→ CBinaryFile バイナリモードでファイルを扱うクラス。バイナリモードでしか使わない処理をここに書く。
 └→ CTextFile テキストモードでファイルを扱うクラス。テキストモードでしか使わない処理をここに書く。

 これからは、これらのクラスをこのように扱っていきたいと思います。


 今回の要点です。


 それでは、次回まで。


第17章 派生と構築2 | 第19章 動的オブジェクト

Last update was done on 2000.8.25

この講座の著作権はロベールが保有しています