複雑な、単一継承 part 1

今回は単一継承と仮想関数の両方を使って、クラスを設計してみることにします。一度に書くと長くなるので、まずは「仮想関数がないと困る」ということを例示したいと思います。最初に、メンバ関数とは何ぞや、ということを説明するために、クラスにメンバ関数を追加した場合のメモリレイアウトを示します。

class cA
{
public:
    int a1;
    int a2;
    int funcA1( void );
    int funcA2( int i );
};

/*
[ a1 ][ a2 ] // cA in mem
*/

struct sA
{
    int a1;
    int a2;
};

int sA::funcA1( sA *this );
int sA::funcA2( sA *this, int i );

メンバ関数を追加しても、メモリレイアウトは何も変わりません。ただ、class-name::function-name の形式の名前を持つ関数が宣言されるだけです。この関数の第一引数は、必ず呼び出し元オブジェクトを指すポインタになります。C++ では、このポインタは自動的に渡されます。ただし、C で C++ のメンバ関数を「エミュレーション」する場合は :: を関数名として使えないので、アンダースコアなどの別の文字に置き換える必要があります。次に、その例を示します。

// C++

int cA::funcA1( void ){ return 0; }
int cA::funcA2( int i ){ return i; }

void func_in_cpp( void )
{
    cA a;
    a.funcA2( 5 );
}

/* C */

int sA_funcA1( sA *this ){ return 0; }
int sA_funcA2( sA *this, int i ){ return i; }

void func_in_c( void )
{
    struct sA a;
    sA_funcA2( &a, 5 );
}

次は、cA から cB を派生させます。

class cB : public cA
{
public:
    int b1;
    int b2;
    int funcB1( void );
    int funcB2( int i );
};

/*
[ a1 ][ a2 ][ b1 ][ b2 ] // cB in mem
*/

struct sB
{
    struct sA a;
    int b1;
    int b2;
};

int sA::funcA1( sA *this );
int sA::funcA2( sA *this, int i );
int sB::funcB1( sB *this );
int sB::funcB2( sB *this, int i );

ここで再び、メンバ関数の呼び出しを C++ と C で比較します。

// C++

int cA::funcA1( void ){ return 0; }
int cA::funcA2( int i ){ return i; }
int cB::funcB1( void ){ return 10; }
int cB::funcB2( int i ){ return i * 5; }

void func_in_cpp( void )
{
    cB b;
    b.funcA1();
    b.funcB2( 5 );
}

/* C */

int sA_funcA1( sA *this ){ return 0; }
int sA_funcA2( sA *this, int i ){ return i; }
int sB_funcB1( sB *this ){ return 10; }
int sB_funcB2( sB *this, int i ){ return i * 5; }

void func_in_c( void )
{
    struct sB b;
    sA_funcA1( &b );
    sB_funcB2( &b, 5 );
}

C のコードで、sA_funcA1( sA *this ) に対して &b が渡されていますが、これが合法である理由は前回説明したので省略します。例が示すとおり、メンバ関数をいくら追加しても、特殊な名前の関数が宣言されるだけで、オブジェクトのサイズは変わりません。さて、このまま関数を追加していってもよいのですが、C++ には「関数のオーバーライド」という機能があります。「オーバーロード」という言葉もありますが、どちらにしても、同じ名前の関数を定義する機能のことです。ここでは簡単に説明しますが、例えばオーバーロードを使うと、次のようなことができます。

// C++

void swapv( int *pa, int *pb )
{
    int temp = *pa;
    *pa = *pb;
    *pb = temp;
}

void swapv( double *pa, double *pb )
{
    double temp = *pa;
    *pa = *pb;
    *pb = temp;
}

void func( void )
{
    int i1 = 0, i2 = 10;
    double d1 = 0.0, d2 = 1.0;

    swapv( &i1, &i2 ); // call swapv( int, int )
    swapv( &d1, &d2 ); // call swapv( double, double )
}

同じ swapv という名前を持つ 2 つの関数が存在していますが、コンパイラが引数の型や個数を参照して、適切な一方を選びます。2 つ以上の関数が存在しても判別可能です。オーバーロードを使わない場合、引数に応じて別の名前の関数を宣言しなければなりません。(この swapv のような関数は、さらに「テンプレート」という機能を使ったほうがすっきりしますが、それについては市販の解説書などを参照してください。)では次にオーバーライドの例を示します。

class cName
{
public:
    char first-name[ 128 ]; // 名前
    char last-name[ 128 ];  // 名字
    void SetName( 
        const char *lpszFirstName, 
        const char *lpszLastName )
    {
        strcpy( first-name, lpszFirstName );
        strcpy( last-name, lpszLastName );
    }
    void GetName( char *lpszBuffer )
    {
        strcpy( lpszBuffer, first-name );
        strcat( lpszBuffer, " " );
        strcat( lpszBuffer, last-name );
    }
};

class cJPNName : public cName
{
public:
    void GetName( char *lpszBuffer )
    {
        strcpy( lpszBuffer, last-name );
        strcat( lpszBuffer, " " );
        strcat( lpszBuffer, first-name );
    }
};

cName は名前を管理するクラスで、GetName 関数は lpszBuffer が指すメモリ領域に対して「名前 名字」の形式で文字列をコピーします。一方、日本などでは「名字 名前」の形式になっているので、GetName を書き換える必要があります。こうすることで、呼び出し元オブジェクトが何であるかによって、適切なクラスの GetName が選択されます。つまりオーバーライドとは、基底クラスのメンバ関数を、派生クラスで書き換えるということです。一方、既にそのクラス、または基底クラスで宣言されている関数と同じ名前で引数が違う関数を宣言することをオーバーロードと呼びます。

// C++

void func( void )
{
    char lpsz[ 256 ];

    cName name;
    cJPNName j_name;

    name.SetName( "Michael", "Anderson" );
    j_name.SetName( "信長", "織田" );

    name.GetName( lpsz );   // call cName::GetName
    j_name.GetName( lpsz ); // call cJPNName::GetName
}

正確に言えば、メンバ関数がオーバーライドされている場合にどのクラスの関数を呼び出すかを決めるのは、オブジェクトの型ではなく呼び出し元となる変数の型によります。そのため、次の問題が生じます。

void func( void )
{
    char lpsz[ 256 ];

    cName name;
    cJPNName j_name;
    cName *pName = NULL;

    pName = &name;
    pName->SetName( "Michael", "Anderson" );
    pName->GetName( lpsz ); // call cName::GetName

    pName = &j_name;
    pName->SetName( "信長", "織田" );
    pName->GetName( lpsz ); // call cName::GetName
}

pName が実際に指しているオブジェクトが何であるかにかかわらず、pName からは cName::GetName が呼び出されてしまうことに注意してください。この例の場合は GetName を呼び出した後で lpsz == "織田 信長" となるべきところが、lpsz == "信長 織田" となってしまいます。pName が実際には cJPNName オブジェクトを指しているなら、cJPNName で定義した関数を呼び出すのが自然で、この問題を解決するのが仮想関数ですが、それは次回。