CプログラマのためのC++オブジェクト指向入門

さて今回はC++と「オブジェクト指向」について簡単に話してみましょう
オブジェクト指向」の話は難しそうだからと拒否反応を示す人もいるかもしれませんね。でも本当は全く難しいことなんてないんです
その証拠に Fig.1 を見てください

Fig.1

FILE*   fpi = fopen( "filename", "rb" );
if( fpi ){
    long    lFileSize = -1;
    fseek( fpi, 0, SEEK_END );
    lFileSize = ftell( fpi );
    if( lFileSize > 0 ){
        size_t  cbBuffer = ( size_t )lFileSize;
        char*   pbBuffer = malloc( cbBuffer );
        if( pbBuffer ){
            fseek( fpi, 0, SEEK_SET );
            fread( pbBuffer, cbBuffer, 1, fpi );
        
            ...
        
            free( pbBuffer );
        }
    }
    fclose( fpi );
}

これは昔からあるC言語のコードですね。これならきっと拒否反応はないと思います
でもこんなコードの中にも実は「オブジェクト指向」があります

それは FILE* です
この FILE* とは何者でしょうか
この FILE* は、それを使う側のプログラマにとってシンタックス的には単なる「詳細不明な構造体へのポインタ」ですが、セマンティクス的には「ファイルを抽象化したオブジェクト」ですね
「ファイルを抽象化したオブジェクト」なんて難しく言わないで「ファイルを操作するために必要なデータの塊」と言っても同じことですが、そう言ってしまっては「オブジェクト指向」になりません
これは逆に言えば「そう言わなければオブジェクト指向になる」と云うことです。つまり「オブジェクト指向」なんて云うのはただプログラマが「それ」を「データの塊」と考えるか「オブジェクト」と考えるかの違いくらいしかないということです
そこで、今、個々の FILE* が指すデータの塊をオブジェクトのインスタンスと呼び、fread や ftell, fgets などの FILE* を引数に取る関数群をインターフェイスと呼べば、何だか「オブジェクト指向」な気分になってきませんか?
そうです。これは本当に「オブジェクト指向」なのです
どうでしょうか。こう考えれば「オブジェクト指向」なんて難しくもなんともなんですよね
慣れないC++の文法に惑わされずに昔馴染みのC言語の文法で見てみると「オブジェクト指向」なんて簡単じゃないですか?

さて、では「オブジェクト指向」が怖くなくなったところでここからはC++です
まずは Fig.1 でも使ったC言語の標準入出力関数群をC++のクラスでラッピングしてみるとどうなるかやってみましょう

まずは「インターフェイス」です
Fig.1 に出てくる FILE* を引数に取る関数は fopen, fseek, ftell, fread, fclose の5つですから、これらをメソッド(メンバ関数)とするクラスを考えると Fig.2 のようになるでしょうか

Fig.2

class CFile {
public:
    bool    Open( const char*   path, const char*    fopen_mode );
    void    Close();
    size_t  Read( void* pBuffer, size_t cbBlock, size_t cBlocks );
    long    Seek( long  lOffset, int    nFlags );
    long    Tell();
};

ではこれを実装してみましょう
Fig.3

class CFile {
    FILE*   m_fp;
public:
    bool    Open( const char*   path, const char*    fopen_mode );
    void    Close();
    size_t  Read( void* pBuffer, size_t cbBlock, size_t cBlocks );
    int     Seek( long  lOffset, int    nFlags );
    long    Tell();
    
    CFile();
    ~CFile();
};
CFile::CFile():
m_fp(NULL)
{
}
CFile::~CFile(){
}
bool    CFile::Open( const char*    path, const char*    fopen_mode ){
    bool    fSuccess = true;
    if( m_fp ){
        fSuccess = false;
    }
    if( fSuccess ){
        m_fp = fopen( path, fopen_mode );
        if( !m_fp ){
            fSuccess = false;
        }
    }
    return  fSuccess;
}
void    CFile::Close(){
    if( m_fp ){
        fclose( m_fp );
        m_fp = NULL;
    }
}
size_t  CFile::Read( void*  pBuffer, size_t cbBlock, size_t cBlocksToRead ){
    size_t  cBlocksRead = 0;
    if( m_fp ){
        cBlocksRead = fread( pBuffer, cbBlock, cBlocksToRead, m_fp );
    }
    return  cBlocksRead;
}
int CFile::Seek( long   lOffset, int   nFlags ){
    int result = -1;
    if( m_fp ){
        result = fseek( m_fp, lOffset, nFlags );
    }
    return  result;
}
long    CFile::Tell(){
    long    lOffset = -1;
    if( m_fp ){
        lOffset = ftell( m_fp );
    }
    return  lOffset;
}

どうでしょうか。中身はC言語でお馴染みの標準入出力関数ですが、外見はすっかりC++になりましたね
ではこのクラスを Fig.1 のコードに適用してみましょう
Fig.4

CFile file;
if( file.Open( "filename", "rb" )){
    file.Seek( 0, SEEK_END );
    long    lFileSize = file.Tell();
    if( lFileSize > 0 ){
        size_t  cbBuffer = ( size_t )lFileSize;
        char*   pbBuffer = malloc( lFileSize );
        if( pbBuffer ){
            file.Seek( 0, SEEK_SET );
            file.Read( pbBuffer, cbBuffer, 1 );
            
            ...
            
            free( pbBuffer );
        }
    }
    file.Close();
}

見た目は Fig.1 とあまり変わらないですね
違う所と云えば引数に指定していた FILE* がなくなって、関数名の前に CFileクラスのインスタンスが入ってるくらいでしょうか
C++もこうしてみるとC言語と大して変わらないと思えてくるのではないでしょうか

さてではここで、コンパイラが Fig.4 のコードをどのように展開するかを考えてみましょう

C++の static でないメソッド(メンバ関数)はクラスではなくインスタンスに関連付けられているものですから、コンパイラはそこで指定されているインスタンスの静的な型から実際に呼び出すべき関数の名前を決定します
またC++クラスの static でないメソッド(メンバ関数)には自分自身のインスタンスを指す暗黙のポインタ変数 this がありますが、これはそのメソッド(メンバ関数)の暗黙の第一引数として実装されています
この2つを踏まえて Fig.3 のコードをC言語風に書くと Fig.5 の様になるでしょうか
Fig.5

struct CFile {
    FILE*   m_fp;
};
CFile_CFile( CFile* const    this ){
    this->m_fp = NULL;
}
CFile_~CFile( CFile* const    this ){
}
bool    CFile_Open( CFile* const    this, const char*   path, const char*    fopen_mode ){
    bool    fSuccess = true;
    if( this->m_fp ){
        fSuccess = false;
    }
    if( fSuccess ){
        this->m_fp = fopen( path, fopen_mode );
        if( !this->m_fp ){
            fSuccess = false;
        }
    }
    return  fSuccess;
}
void    CFile_Close( CFile* const    this ){
    if( this->m_fp ){
        fclose( this->m_fp );
        this->m_fp = NULL;
    }
}
size_t  CFile_Read( CFile* const    this, void* pBuffer, size_t cbBlock, size_t cBlocksToRead ){
    size_t  cBlocksRead = 0;
    if( this->m_fp ){
        cBlocksRead = fread( pBuffer, cbBlock, cBlocksToRead, this->m_fp );
    }
    return  cBlocksRead;
}
int CFile_Seek( CFile* const    this, long  lOffset, int    nFlags ){
    int result = -1;
    if( this->m_fp ){
        result = fseek( this->m_fp, lOffset, nFlags );
    }
    return  result;
}
long    CFile_Tell( CFile* const    this ){
    long    lOffset = -1;
    if( this->m_fp ){
        lOffset = ftell( this->m_fp );
    }
    return  lOffset;
}

ここで1つおもしろいことに気付きませんか?
Fig.5 の CFile構造体の宣言からはメソッド(メンバ関数)がなくなっていますね
これは「C++ではクラス宣言のときにメソッド(メンバ関数)をクラスの {} の中に書くけれども、実際にはその中にメソッド(メンバ関数)の実体があるわけではない」ということなのです
つまりクラスのメソッド(メンバ関数)と言っても暗黙の第一引数に this を取るだけの普通の関数だと云うことです

さて、先程コンパイラはメソッド名(メンバ関数名)の前に指定されているインスタンスの静的な型から際に呼び出すべき関数の名前を決定すると言いました
しかしここで、例えば Fig.6 のような状況ではどうでしょうか
Fig.6

class CFile2 : public CFile {
    char    m_path[MAX_PATH];
public:
    bool    Open( const char*   path, const char*    fopen_mode );
    void    Close();
    
    CFile2();
    ~CFile2();
};
CFile2::CFile2():
CFile()
{
    m_path[0] = '\0';
}
CFile2::~CFile2(){
}
bool    CFile2::Open( const char*   path, const char*    fopen_mode ){
    bool    fSuccess = CFile::Open( path, fopen_mode );
    if( fSuccess ){
        strcpy( m_path, path );
        printf( "%s: file open succeeded.\n", m_path );
    }
    return  fSuccess;
}
void    CFile2::Close(){
    CFile::Close();
    printf( "%s: file closed.\n", m_path );
    m_path[0] = '\0';
}
bool    FileOpenTest( CFile*    pFile, const char*  path, const char*    fopen_mode ){
    bool    fSuccess = pFile->Open( path, fopen_mode );
    if( fSuccess ){
        pFile->Close();
    }
    return  fSuccess;
}
CFile2  file2;
if( FileOpenTest( &file2, "filename", "rb" )){
    // Open成功
}

CFile クラスから新たに CFile2 という「派生クラス」を定義しました
そしてその CFile2 クラスのインスタンス file2 を作ります
ここでそのインスタンス file2 のアドレスを FileOpenTest という関数に渡してみましょう
FileOpenTest関数の引数は CFile* となっていますが、CFile2 は CFile の派生クラスなので CFile* を期待しているところに CFile2* を渡すことは全く問題ありません
しかし、この Fig.6 は期待通りの動きはしないでしょう
実際に実行してみると FileOpenTest関数から呼び出されるのは CFile2::Open と CFile2::Close ではなく、CFile::Open と CFile::Close が呼び出されてしまっていることがわかると思います

これはなぜでしょうか
なぜなら、コンパイラはそこに指定されているインスタンスの静的な型を見て実際に呼び出すべき関数の名前を決定しているので、pFile の型、即ち CFile のメソッド(メンバ関数)が呼び出されてしまうからです
これはC言語風に書けば Fig.7 の様になっていると言えるでしょうか
Fig.7

bool    FileOpenTest( CFile*    pFile, const char*  path, const char*    fopen_mode ){
    bool    fSuccess = CFile_Open( pFile, path, fopen_mode );
    if( fSuccess ){
        CFile_Close( pFile );
    }
    return  fSuccess;
}

FileOpenTest関数の中では予め決まった関数(CFile::Open と CFile::Close)が呼ばれるようになっています
これでは折角 pFile に CFile2 のインスタンスを渡しているのが全く無意味になってしまっていますね
さて困りました
ここで pFile に CFile* を渡した時には CFile::Open と CFile::Close, CFile2* を渡した時には CFile2::Open と CFile2::Close と云うように、指定されたインスタンスの実際の型に応じて自動的に適切なメソッド(メンバ関数)が呼び出されるようにすることはできないのでしょうか

もちろんできます
C++にはこういった状況を解決する為に用意されているものがあります
それは「仮想関数」です
Fig.8 の様に CFile のクラス宣言のメソッド(メンバ関数)の前にキーワード「virtual」を指定してみましょう
Fig.8

class CFile {
    FILE*   m_fp;
public:
    virtual bool    Open( const char*   path, const char*    fopen_mode );
    virtual void    Close();
    virtual size_t  Read( void* pBuffer, size_t cbBlock, size_t cBlocks );
    virtual int     Seek( long  lOffset, int    nFlags );
    virtual long    Tell();
    
    CFile();
    ~CFile();
};

すると Fig.6 の FileOpenTest は期待通り CFile2::Open と CFile2::Close を呼び出すようになります
これは一体どう云う仕組みなのでしょうか
コンパイラコンパイルする過程をまたC言語風に書いてみると、CFile は Fig.9 のようになり、CFile2 は Fig.10 のようになります
Fig.9

struct CFileVtbl {
    bool    (*Open)( CFile* const this, const char*   path, const char*    fopen_mode );
    void    (*Close)( CFile* const    this );
    size_t  (*Read)( CFile* const this, void* pBuffer, size_t cbBlock, size_t cBlocks );
    int     (*Seek)( CFile* const this, long  lOffset, int    nFlags );
    long    (*Tell)( CFile* const this );
};
bool    CFile_Open( CFile* const    this, const char*   path, const char*    fopen_mode ){
    bool    fSuccess = true;
    if( this->m_fp ){
        fSuccess = false;
    }
    if( fSuccess ){
        this->m_fp = fopen( path, fopen_mode );
        if( !this->m_fp ){
            fSuccess = false;
        }
    }
    return  fSuccess;
}
void    CFile_Close( CFile* const    this ){
    if( this->m_fp ){
        fclose( this->m_fp );
        this->m_fp = NULL;
    }
}
size_t  CFile_Read( CFile* const    this, void* pBuffer, size_t cbBlock, size_t cBlocksToRead ){
    size_t  cBlocksRead = 0;
    if( this->m_fp ){
        cBlocksRead = fread( pBuffer, cbBlock, cBlocksToRead, this->m_fp );
    }
    return  cBlocksRead;
}
int CFile_Seek( CFile* const    this, long  lOffset, int    nFlags ){
    int result = -1;
    if( this->m_fp ){
        result = fseek( this->m_fp, lOffset, nFlags );
    }
    return  result;
}
long    CFile_Tell( CFile* const    this ){
    long    lOffset = -1;
    if( this->m_fp ){
        lOffset = ftell( this->m_fp );
    }
    return  lOffset;
}
const CFileVtbl c_CFileVtbl = {
    CFile_Open,
    CFile_Close,
    CFile_Read,
    CFile_Seek,
    CFile_Tell,
};
struct CFile {
    const CFileVtbl* const  lpVtbl;
    FILE*   m_fp;
};
CFile_CFile( CFile* const    this ){
    this->lpVtbl = &c_CFileVtbl;
    this->m_fp = NULL;
}
CFile_~CFile( CFile* const    this ){
}

Fig.10

struct CFile2Vtbl {
    // まず最初に基底クラスである CFile の仮想関数テーブルをここに
    bool    (*Open)( CFile* const this, const char*   path, const char*    fopen_mode );
    void    (*Close)( CFile* const    this );
    size_t  (*Read)( CFile* const this, void* pBuffer, size_t cbBlock, size_t cBlocks );
    int     (*Seek)( CFile* const this, long  lOffset, int    nFlags );
    long    (*Tell)( CFile* const this );
    // 基底クラスの後ろに CFile2 で新たに追加した仮想関数を置く
    // (でも CFile2 で追加された仮想関数は無い)
};
const CFile2Vtbl c_CFile2Vtbl = {
    CFile2_Open,
    CFile2_Close,
    CFile_Read,
    CFile_Seek,
    CFile_Tell,
};
struct CFile2 {
    // まず最初に基底クラスである CFile を丸ごとここに置く
    // (ただし仮想関数テーブルだけは CFile2 に差し替える)
    const CFile2Vtbl* const  lpVtbl;
    FILE*   m_fp;
    // 基底クラスの後ろに CFile2 のメンバ変数を置く
    char    m_path[MAX_PATH];
};
CFile2_CFile2( CFile2* const    this ){
    // コンストラクタの先頭では必ず基底クラスのコンストラクタがまず最初に呼ばれる
    CFile_CFile(( CFile* )this );
    // その後で自分の初期化
    this->lpVtbl = &c_CFile2Vtbl;
    m_path[0] = '\0';
}
CFile2_~CFile2( CFile2* const    this ){
    // デストラクタの最後では必ず基底クラスのデストラクタが呼ばれる
    CFile_~CFile(( CFile* )this );
}

CFile構造体も CFile2構造体も先頭に新しく CFileVtbl, CFile2Vtblと云う構造体へのポインタが増えてますね
CFileVtbl構造体も CFile2Vtbl構造体も見れば分かる通りこれは関数ポインタのテーブルですね
別に覚える必要はありませんがこれは「仮想関数」のテーブルなので「仮想関数テーブル」と言うこともあります
コンパイラは今呼び出そうとしているメソッド(メンバ関数)が「仮想関数」である場合には、インスタンスの「仮想関数テーブル」を参照するようなコードを出力します
つまりC言語風に書けば Fig.11 の様になっていると言えるでしょうか
Fig.11

bool    FileOpenTest( CFile*    pFile, const char*  path, const char*    fopen_mode ){
    bool    fSuccess = pFile->lpVtbl->Open( pFile, path, fopen_mode );
    if( fSuccess ){
        pFile->lpVtbl->Close( pFile );
    }
    return  fSuccess;
}

どうでしょう。こうしてみると「仮想関数」も「継承」も簡単じゃないですか?

さて、「仮想関数」も「継承」も分かったところで少し練習しておきましょう
実は Fig.8 はこのままの実装では問題になるケースがあります
それは何でしょうか

その答えは、まず Fig.12 を見てください
Fig.12

void    delete_CFile( CFile*   pFile ){
    if( pFile ){
        delete pFile;
    }
}

この様な関数があったときこれをコンパイラはどのようなコードに展開するでしょうか
メンバ関数を呼び出すとき、コンパイラインスタンスの静的な型を見て実際に呼び出すべき関数の名前を決定しているんでしたね
明示的には指定されていませんが、Fig.12 でも実はあるメンバ関数が呼び出されています
それはデストラクタ ~CFile です
これもまたC言語風に書いてみましょう
Fig.13

void    delete_CFile( CFile*   pFile ){
    if( pFile ){
        CFile_~CFile( pFile );
        free( pFile );
    }
}

さあもう分かったのではないでしょうか
先程と同じようにこの関数に CFile2 のインスタンスを渡してみると本当はここで CFile2_~CFile2 を呼び出して欲しいのに常に CFile_~CFile が呼ばれるようになってしまっていますね
これが問題となるケースです
こんなときはどうするのかはもうお分かりですね
そうです。こんなときは virtual を指定するんでしたね
ですから、Fig.8 は Fig.14 のようにデストラクタにも virtual を指定する必要がありました
Fig.14

class CFile {
    FILE*   m_fp;
public:
    virtual bool    Open( const char*   path, const char*    fopen_mode );
    virtual void    Close();
    virtual size_t  Read( void* pBuffer, size_t cbBlock, size_t cBlocks );
    virtual int     Seek( long  lOffset, int    nFlags );
    virtual long    Tell();
    
    CFile();
    virtual ~CFile();
};

C++ で「基底クラスのデストラクタには必ず virtual を指定しろ」と言われるのはこのためなのです

それでは今回はこの辺で終わりにしましょう
最後に1つ
今回サンプルに使った CFile と云うクラスですが、実際には Fig.15 の様にするのがお薦めです
なぜこれがお薦めなのかはまた次回以降の講義で

Fig.15

struct IFile {
    virtual long    AddRef() = 0;
    virtual long    Release() = 0;
    virtual bool    Open( const char*   path, const char*    fopen_mode ) = 0;
    virtual void    Close() = 0;
    virtual size_t  Read( void* pBuffer, size_t cbBlock, size_t cBlocks ) = 0;
    virtual long    Seek( long  lOffset, int    nFlags ) = 0;
    virtual long    Tell() = 0;
};
IFile*  FileOpen( const char*   path, const char*    fopen_mode );

class CFile : public IFile {
    long    m_cRef;
    FILE*   m_fp;
public:
    virtual long    AddRef();
    virtual long    Release();
    virtual bool    Open( const char*   path, const char*    fopen_mode );
    virtual void    Close();
    virtual size_t  Read( void* pBuffer, size_t cbBlock, size_t cBlocks );
    virtual int     Seek( long  lOffset, int    nFlags );
    virtual long    Tell();
    
    static CFile*   CreateInstance();
    
    CFile();
    virtual ~CFile();
};
CFile*   CFile::CreateInstance(){
    return new CFile;
}
CFile::CFile():
m_cRef(1),
m_fp(NULL)
{
}
CFile::~CFile(){
    Close();
}
long    CFile::AddRef(){
    return  m_cRef++;
}
long    CFile::Release(){
    if( --m_cRef == 0 ){
        delete this;
        return  0;
    }
    return  m_cRef;
}
bool    CFile::Open( const char*    path, const char*    fopen_mode ){
    bool    fSuccess = true;
    if( m_fp ){
        fSuccess = false;
    }
    if( fSuccess ){
        m_fp = fopen( path, fopen_mode );
        if( !m_fp ){
            fSuccess = false;
        }
    }
    return  fSuccess;
}
void    CFile::Close(){
    if( m_fp ){
        fclose( m_fp );
        m_fp = NULL;
    }
}
size_t  CFile::Read( void*  pBuffer, size_t cbBlock, size_t cBlocksToRead ){
    size_t  cBlocksRead = 0;
    if( m_fp ){
        cBlocksRead = fread( pBuffer, cbBlock, cBlocksToRead, m_fp );
    }
    return  cBlocksRead;
}
int CFile::Seek( long   lOffset, int   nFlags ){
    int result = -1;
    if( m_fp ){
        result = fseek( m_fp, lOffset, nFlags );
    }
    return  result;
}
long    CFile::Tell(){
    long    lOffset = -1;
    if( m_fp ){
        lOffset = ftell( m_fp );
    }
    return  lOffset;
}
IFile*  FileOpen( const char*   path, const char*    fopen_mode ){
    IFile*  pFile = CFile::CreateInstance();
    if( pFile ){
        if( !pFile->Open( path, fopen_mode )){
            pFile->Release();
            pFile = NULL;
        }
    }
    return  pFile;
}
IFile*  fpi = FileOpen( "filename", "rb" );
if( fpi ){
    fpi->Seek( 0, SEEK_END );
    long    lFileSize = fpi->Tell();
    if( lFileSize > 0 ){
        char*   pbBuffer = malloc( lFileSize );
        if( pbBuffer ){
            fpi->Seek( 0, SEEK_SET );
            fpi->Read( pbBuffer, cbBuffer, 1 );
        
            ...
        
            free( pbBuffer );
        }
    }
    fpi->Release();
    fpi = NULL;
}