第54章 融合2

 前回に引き続き仮想継承について話していきます。バッファを読み書きするクラスを例にじっくり考えていきましょう。


 では、今回の要点です。


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


 今回から数章に渡って、あるバッファを読み書きするクラス CBufferRW を作ってみましょう。

 このクラスをどうやって設計するかは人それぞれだと思いますが、今回は次のようにしてみたいと思います。

CBuffer
CBufferRead CBufferWrite
CBufferRW

 バッファクラス CBuffer を基底に、バッファを読むクラス CBufferReadバッファに書くクラス CBufferWrite を経由して CBufferRW クラスを作りたいと思います。

 バッファは CBuffer で作るわけですが、仮想継承しないと読むときと書くときでバッファが別になってしまいます。ここでは仮想継承が必須になりますね。

 また、読むだけで構わないときは CBufferRead を、書くだけで構わないときは CBufferWrite を使うこともできます。故意に読めないように、書けないようにしたいときには便利ですね。


 では、骨格を作ってみましょう。

プログラム1
// BufferRW.h
#ifndef __BUFFERRW_H__INCLUDED__
#define __BUFFERRW_H__INCLUDED__

class CBuffer
{
protected:
    unsigned char* m_pBuffer;  // バッファ
    int            m_nSize;    // バッファサイズ
    int            m_nPos;     // 現在位置

public:
    CBuffer(int nSize);
    virtual ~CBuffer();

public:
    int SetPos(int nPos);  // 位置設定
    int AddPos(int nAdd);  // 位置を進ませる
};

class CBufferRead : virtual public CBuffer
{
public:
    CBufferRead(int nSize);

public:
    // バッファから読み出す
    int Read(void* pRead, int nReadSize,
             int nUnit = 1);
};

class CBufferWrite : virtual public CBuffer
{
public:
    CBufferWrite(int nSize);

public:
    // バッファに書き込む
    int Write(const void* pWrite, int nWriteWord,
              int nUnit = 1);
};

class CBufferRW : public CBufferRead,
                  public CBufferWrite
{
public:
    CBufferRW(int nSize);
};

#endif // #ifndef __BUFFERRW_H__INCLUDED__
プログラム2
// BufferRW.cpp
#include "BufferRW.h"
#include <memory.h>
#include <new>

CBuffer::CBuffer(int nSize)
    : m_pBuffer(NULL), m_nSize(0), m_nPos(0)
{
    m_pBuffer = new unsigned char[nSize];
    if(m_pBuffer == NULL)
        throw std::bad_alloc();  // 例外を投げます
    m_nSize = nSize;

    memset(m_pBuffer, 0, nSize);
}

CBuffer::~CBuffer()
{
    if(m_pBuffer != NULL)
    {
        delete [] m_pBuffer;
        m_pBuffer = NULL;  // 一応、ね
    }
}

// 位置設定
int CBuffer::SetPos(int nPos)
{
    if(nPos > m_nSize)
        m_nPos = m_nSize;
    else if(nPos < 0)
        m_nPos = 0;
    else
        m_nPos = nPos;
    return m_nPos;
}

// 位置を進ませる
int CBuffer::AddPos(int nAdd)
{
    return SetPos(m_nPos + nAdd);
}

CBufferRead::CBufferRead(int nSize) : CBuffer(nSize){ }

// バッファから読み出す
int CBufferRead::Read(
    void* pRead, int nReadWord, int nUnit)
{
    // 残り単位
    const int nLeftWord = (m_nSize - m_nPos) / nUnit;
    if(nReadWord > nLeftWord)  // 読み込みすぎのとき
        nReadWord = nLeftWord;

    // 読み込みサイズ
    const int nReadSize = nReadWord * nUnit;
    memcpy(pRead, m_pBuffer + m_nPos, nReadSize);
    m_nPos += nReadSize;
    return nReadWord;
}

CBufferWrite::CBufferWrite(int nSize) : CBuffer(nSize){ }

// バッファに書き込む
int CBufferWrite::Write(
    const void* pWrite, int nWriteWord, int nUnit)
{
    // 残り単位
    const int nLeftWord = (m_nSize - m_nPos) / nUnit;
    if(nWriteWord > nLeftWord)  // 読み込みすぎのとき
        nWriteWord = nLeftWord;

    // 読み込みサイズ
    const int nWriteSize = nWriteWord * nUnit;
    memcpy(m_pBuffer + m_nPos, pWrite, nWriteSize);
    m_nPos += nWriteSize;
    return nWriteWord;
}

 作ったメンバ変数・メンバ関数は次の通りです。

変数・関数名 機能
CBuffer::CBuffer CBuffer のコンストラクタです。バッファのサイズを渡して、バッファを作成します。
CBuffer::~CBuffer CBuffer のデストラクタです。バッファを解放します。
CBuffer::m_pBuffer バッファへのポインタです。
CBuffer::m_nSize バッファのサイズです。
CBuffer::m_nPos 読み書きを始める位置です。ファイルのファイルポインタと同じようなものだと考えてください。
CBuffer::SetPos m_nPos を変更します。絶対位置を指定します。
新しい位置を返します。
CBuffer::AddPos m_nPos を変更します。m_nPos からの相対位置を指定します。
新しい位置を返します。
CBufferRead::CBufferRead CBufferRead のコンストラクタです。CBuffer のコンストラクタを呼ぶだけです。
CBufferRead::Read バッファから読み出します。
読み出すサイズには倍率を指定できます。例えば int のデータを10個読み出したい場合は、buf.Read(buffer, 10, sizeof int); のようにします。
戻り値は実際に読み込んだデータの個数になります。サイズを倍率で割った数になります。
CBufferWrite::CBufferWrite CBufferWrite のコンストラクタです。CBuffer のコンストラクタを呼ぶだけです。
CBufferWrite::Write バッファに書き込みます。
書き込むサイズには倍率を指定できます。例えば double のデータを7個書き込みたい場合は、buf.Write(buffer, 7, sizeof double); のようにします。
戻り値は実際に書き込んだデータの個数になります。サイズを倍率で割った数になります。
CBufferRW::CBufferRW CBufferRW のコンストラクタですが...。

 どういう実装になっているかはとりあえず置いておいて、この表だけ把握してもらえれば充分です。


 さて、ここで CBufferRW のコンストラクタを見て下さい。まだ実装していません。ここでどういう実装にすればいいのか考えてみましょう。

 CBufferRW は CBufferRead と CBufferWrite の両方から派生しているので CBufferRead と CBufferWrite の両方のコンストラクタを呼ぶ必要があります。

 しかし、CBuffer のコンストラクタはどうなるのでしょうか? CBufferRead と CBufferWrite の両方のコンストラクタから呼ばれてしまうと、2回呼ばれることになります。となるとバッファの確保が2回行われ、2回目の確保の時に1回目のアドレスを破棄するのでメモリリークを起こすはずです。

 このように、二重に初期化してしまわないように、CBuffer のコンストラクタは CBufferRW から CBufferRead や CBufferWrite のコンストラクタを呼んでも呼ばれないようになっています

 そうなると、CBufferRW では CBuffer のコンストラクタが呼ばれないことになってしまうので、CBuffer のコンストラクタは CBufferRW でも呼ぶ必要があります

 つまり、次のようになるわけです。

CBufferRW::CBufferRW(int nSize)
    : CBuffer(nSize), CBufferRead(nSize), CBufferWrite(nSize)
{
}

 こうすると、先ず CBuffer のコンストラクタが呼ばれ、その次に CBufferRead と CBufferWrite のコンストラクタが呼ばれるわけです。CBuffer のコンストラクタは1回しか呼ばれず、メモリリークを起こすことはありません。

 このように、仮想継承されたクラスのコンストラクタは、2つ以上先の派生クラスでも呼ぶ必要があります。CBufferRW から派生するクラスでも、CBuffer のコンストラクタを呼ぶ必要があります。

 もちろん、CBufferRead のオブジェクトを作るときには CBuffer のコンストラクタは呼ばれます。上の話はコロンの後で基底クラスのコンストラクタを呼ぶときの話です。


 では、今回の要点です。


 では、次回まで。


第53章 融合 | 第55章 融合3

Last update was done on 2001.6.5

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