第21章 メンバの呼び分け

 今回は新しいクラスを作ります。と言っても、CBinaryFile から派生するクラスです。その時ちょっと困ることが起こるので、それを解決してみましょう。


 では、今回の要点です。


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


 今回作るクラスは暗号化をサポートしたファイルクラスです。CEncodeFile という名前にしたいと思います。

 暗号化と言ってもそんな大袈裟なものではないです。0xAA とのXORをとってデータの読み書きをするだけです。XORを忘れたという人は第1部第47章を参照して下さい。

 例えば、こんな感じです。

暗号化前 暗号化後
16進 文字 16進 文字
83 7D 83 43 83 67 83 6C 83 8A 83 45 83 80マイトネリウム 29 D7 29 E9 29 CD 29 C6 29 20 29 EF 29 2A).).).).) ).)*
96 76 90 48 8E 71 8E 5F 83 47 83 60 83 8B没食子酸エチル 3C DC 3A E2 24 DB 24 F5 29 ED 29 CA 29 21<.:.$.$.).).)!
93 E0 8C 97 8C 5E 93 64 8E 71 88 DA 93 AE 94 BD 89 9E内圏型電子移動反応 39 4A 26 3D 26 F4 39 CE 24 DB 22 70 39 04 3E 17 23 349J&=&.9.$."p9.>.#4

 XORは指定したビットを反転させるというものでした。なので、2回同じ値でXORをとると元に戻ります。つまり、0xAA とXORをとって暗号化したデータは、もう1回 0xAA とXORをとれば元に戻せるわけですね。

 そして、暗号化してファイルを扱うので、テキストモードにするわけにはいきません。改行コードは別の文字に変わってしまい、代わりにその別の文字が改行コードに変わってしまいます。

 ということで、このクラスは CBinaryFile から派生させることにします。こんな感じですね。

CFile
/      \
CBinaryFile CTextFile
CEncodeFile

 では、早速作ってみましょう。

プログラム1
// File.h
class CFile
{
    // ファイルの読み書き
public:
    virtual size_t Read(void* buffer, size_t nSize);
    virtual size_t Write(const void* buffer, size_t nSize);
};
プログラム2
// EncFile.h
#ifndef __ENCFILE_H__INCLUDED__
#define __ENCFILE_H__INCLUDED__

#include <stddef.h>  // この中に size_t 型の定義があります
#include "BinFile.h"

const size_t EF_OUTOFMEMORY = 0xFFFFFFFF;  // メモリ不足

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

    virtual ~CEncodeFile();

    // ファイルの読み書き
public:
    virtual size_t Read(void* buffer, // 読み出し
    virtual size_t Write(const void* buffer, size_t nSize);  // 書き込み

    // 暗号化・復号化
private:
    virtual void Encode(void* bufDest, const void* bufSrc, size_t nSize);  // 暗号化
    virtual void Decode(void* buffer, size_t nSize);                       // 復号化
};

#endif
プログラム3
// EncFile.cpp
#include <iostream.h>
#include "EncFile.h"

const unsigned char XOR_FACTOR = 0xAA;  // 暗号化に使う数

// デフォルトコンストラクタ
CEncodeFile::CEncodeFile()
{
    cout << "CEncodeFile::CEncodeFile 1" << endl;
}

// 構築と同時にファイルを開く
CEncodeFile::CEncodeFile(const char* pszPath, const char* pszFlags)
{
    cout << "CEncodeFile::CEncodeFile 2" << endl;
    Open(pszPath, pszFlags);
}

// コピーコンストラクタ
CEncodeFile::CEncodeFile(const CEncodeFile& rother) : CBinaryFile(rother)
{
    cout << "CEncodeFile::CEncodeFile 3" << endl;
}

// デストラクタ
CEncodeFile::~CEncodeFile()
{
    cout << "CEncodeFile::~CEncodeFile" << endl;
}

// ファイルの読み出し
size_t CEncodeFile::Read(void* buffer, size_t nSize)
{
    size_t nRead;  // 読み出したバイト数

    nRead = Read(buffer, nSize);
    Decode(buffer, nRead);

    return nRead;
}

// ファイルの書き込み
size_t CEncodeFile::Write(const void* buffer, size_t nSize)
{
    unsigned char* bufEncode;  // 暗号化バッファ
    size_t nWrite;  // 読み出した書き込んだバイト数

    if(nSize == EF_OUTOFMEMORY)
        return EF_OUTOFMEMORY;

    bufEncode = new unsigned char[nSize];
    if(bufEncode == NULL)
        return EF_OUTOFMEMORY;

    Encode(bufEncode, buffer, nSize);
    nWrite = Write(bufEncode, nSize);

    delete [] bufEncode;
    return nWrite;
}

// 暗号化
// bufDest, bufSrc の型は void*, const void* なので、
// unsigned char*, const unsigned char* でキャストしてから参照します
void CEncodeFile::Encode(void* bufDest, const void* bufSrc, size_t nSize)
{
    size_t i;

    for(i = 0; i < nSize; i++)
        ((unsigned char*)bufDest)[i] =
            ((const unsigned char*)bufSrc)[i] ^ XOR_FACTOR;
}

// 復号化(暗号化されたものを元に戻すこと)
void CEncodeFile::Decode(void* buffer, size_t nSize)
{
    size_t i;

    for(i = 0; i < nSize; i++)
        ((unsigned char*)buffer)[i] ^= XOR_FACTOR;
}

 Read と Write は仮想関数にしました。アップキャストしたときに困りますからね。

 で、さらに CBinaryFile にあった ReadAndDump も、CEncodeFile では暗号化がサポートされます。ReadAndDump ではファイルからの読み込みに Read 関数を使っていました。なので、CEncodeFile ではこの部分が CEncodeFile の Read になるわけです。

 ということで、このクラスを使ってみましょう。

プログラム
// TestFile.h
#include <iostream.h>
#include <string.h>
#include "BinFile.h"
#include "EncFile.h"

// ファイル名
static const char szPath[] = "Test.dat";

// データを書き込む
bool Write(CFile& rfile)
{
    char buffer[512];

    cout << "ファイルに書き込むデータを入力して下さい。" << endl;
    cin >> buffer;

    if(rfile.Open(szPath, "w") == false)
        return false;
    if(rfile.Write(buffer, strlen(buffer)) == EF_OUTOFMEMORY)
        return false;
    rfile.Close();

    return true;
}

// データを読み込んで、ダンプする
bool Read(CBinaryFile& rbin)
{
    const int PER_LINE = 16;  // 取得文字数
    char      bufHex[PER_LINE * 3 + 1];
    char      bufASCII[PER_LINE + 1];
    int       nRead;

    if(rbin.Open(szPath, "r") == false)
        return false;

    do
    {
        nRead = rbin.ReadAndDump(bufHex, bufASCII, PER_LINE);
        cout << bufHex << bufASCII << endl;
    }
    while(nRead == PER_LINE);
    rbin.Close();

    return true;
}

int main()
{
    CBinaryFile bin;
    CEncodeFile enc;

    // 暗号化して書き込み
    if(Write(enc) == false)
        return 0;

    // 暗号化されたままで読み出し
    if(Read(bin) == false)
        return 0;

    // 復号化して読み出し
    if(Read(enc) == false)
        return 0;

    return 0;
}
実行結果
CBinaryFile::CBinaryFile 1
CBinaryFile::CBinaryFile 1
CEncodeFile::CEncodeFile 1
ファイルに書き込むデータを入力して下さい。
マイトネリウム・没食子酸エチル・内圏型電子移動反応
(ここでエラーが発生!)

 ありゃ、エラーが出てしまいました。何のエラーかというとスタックオーバーフローです(第1部第61章参照)。大きな自動変数を作った覚えはないのに、何でスタックオーバーフローなんかになったんでしょうね?

 その原因は、実はここにあります。

// ファイルの書き込み
size_t CEncodeFile::Write(const void* buffer, size_t nSize)
{
    // 省略

    nWrite = Write(bufEncode, nSize);

    // 省略
}

 さて、この Write はどのクラスの Write なのでしょうか?

 Write は仮想関数でした。そしてこれは CEncodeFile の Write から呼ばれています。CEncodeFile の Write が呼ばれているので、普通オブジェクトの本来の型は CEncodeFile であるはずです。つまり、この中で呼ばれた Write も CEncodeFile の Write なのです

 となると、これは再帰呼び出しになります(第3部第21章参照)。終了条件のない再帰呼び出しは無限ループになって、スタックオーバーフローになってしまいます。

 基底クラスの Write を呼ぶようにできれば解決するのですが、これはどうしたらいいんでしょうね?


 これには、第1部第68章で出てきたスコープ解決演算子 :: を使います。どう使えばいいかは、何となく分かるとは思いますが、

nWrite = CBinaryFile::Write(bufEncode, nSize);

のようにします。

 Write は仮想関数ですが、こうすれば常に CBinaryFile の Write が呼べるようになります。もちろん、仮想関数にしていないオーバーライド関数でも同じように使えます。

 そして、Read でも同じく

nRead = CBinaryFile::Read(buffer, nSize);

とする必要がありますね。

 では、もう1回プログラムを実行してみましょう。

実行結果
CBinaryFile::CBinaryFile 1
CBinaryFile::CBinaryFile 1
CEncodeFile::CEncodeFile 1
ファイルに書き込むデータを入力して下さい。
マイトネリウム・没食子酸エチル・内圏型電子移動反応
CFile::Open
CBinaryFile::ModifyFlags
CFile::Open
CBinaryFile::ModifyFlags
29 D7 29 E9 29 CD 29 C6 29 20 29 EF 29 2A 2B EF ).).).).) ).)*+.
3C DC 3A E2 24 DB 24 F5 29 ED 29 CA 29 21 2B EF <.:.$.$.).).)!+.
39 4A 26 3D 26 F4 39 CE 24 DB 22 70 39 04 3E 17 9J&=&.9.$."p9.>.
23 34                                           #4
CFile::Open
CBinaryFile::ModifyFlags
83 7D 83 43 83 67 83 6C 83 8A 83 45 83 80 81 45 .}.C.g.l...E...E
96 76 90 48 8E 71 8E 5F 83 47 83 60 83 8B 81 45 .v.H.q._.G.`...E
93 E0 8C 97 8C 5E 93 64 8E 71 88 DA 93 AE 94 BD .....^.d.q......
89 9E                                           ..
CEncodeFile::~CEncodeFile
CBinaryFile::~CBinaryFile
CBinaryFile::~CBinaryFile

 先ず暗号化されたままのデータが表示され、次に復号化されたデータが表示されています。入力した文字は初めに話した例と同じなので、きちんと変換されているか比べてみて下さい。

 このように、

<クラス名>::<メンバ関数>(<実引数リスト>)

とすると、どのオーバーライド関数を呼ぶかを決めることができるのです。

 また、このため CEncodeFile::Write で呼ばれた生の(CBinaryFile:: のついていない)Write は CEncodeFile::Write でないこともあります。CEncodeFile から派生したクラスで Write をオーバーライドし、そのクラスから CEncodeFile::Write を呼ぶと、そのクラスの Write になりますね。実際にはないことだとは思いますが、一応注意しておきます。


 最後に余談です。

 今回の CEncodeFile クラスの Encode, Decode 関数は仮想関数にしてあります。これは、CEncodeFile から別の暗号化ファイルクラスを派生できるようにということでこうしています。

 この Encode, Decode をオーバーライドすれば、簡単に別の暗号化方法を使うクラスを作ることができますね。


 では、今回の要点です。


 長かったですが、要点はこれだけです。もうちょっと短くできればとは思ったのですが、力が及ばなかったです。

 それでは、次回まで。


第20章 デストラクタ(仮) | 第22章 メンバ定数

Last update was done on 2000.8.27

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