仮想関数の特殊な用法

今回は「発展」の意味で、仮想関数のちょっと変わった使い方について、二つほど紹介します。

1, デストラクタ

C では、ヒープからメモリを割り当てるのに、関数の malloc などを使い、解放するのに free を使います。C++ においては malloc/free ではなくて、new/delete を用います。両者は演算子として扱われていますが、実際にはそれぞれ一個の関数になっています。例えば new は、内部で malloc を呼び出してオブジェクトのためのメモリを確保し、そのクラスのコンストラクタを呼び出し、最後にオブジェクトの型に合ったポインタを返します。delete は、オペランド(関数における引数)の型のデストラクタを呼び出し、free でメモリを解放します。コンストラクタとデストラクタの名前は決まっていて、前者はクラスの名前と同一であり、後者はクラスの名前の直前に ~ が付加した名前です。両者の動作は、概念的には次のようになります。

class cA
{
public:
    cA( void ){};  // コンストラクタ
    ~cA( void ){}; // デストラクタ
};

void func( void )
{
    cA *pA = new cA();
    /*
        pA = ( cA* ) malloc( sizeof( cA ) );
        pA->cA();
    */

    delete pA;
    /*
        pA->~cA();
        free( pA );
    */
}

コンストラクタとデストラクタの詳細についてはここでは述べません。専門書を参考にしてください。ここで説明したいのは、デストラクタを仮想関数にすると便利だということです。次に、デストラクタが仮想でない場合の不便さを例示します。

class cA
{
public:
    cA( void ){};
    ~cA( void ){};
};

class cB : public cA
{
public:
    cB( void ){};
    ~cB( void ){};
};

void func( void )
{
    cA *pA = new cA();
    delete pA; // ok

    cB *pB = new cB();
    delete pB; // ok

    cA *pA = new cB();
    delete pA; // error
}

最後の二行にだけ注目してください。new cB() は cB 型オブジェクトへのポインタを返しますが、これを cA *pA に代入することは合法です。しかしこれを解放しようとして delete に渡すと、オペランドの型どおりに cA::~cA() が呼び出されてしまいます。実際に指しているのは cB 型オブジェクトなのだから、cB::~cB() が呼び出されるべきです。どこかで聞いた話ではありませんか?そう、仮想関数です。デストラクタを仮想関数にしてしまえば、次のようなことが実現できます。

class cA
{
public:
    cA( void ){};
    virtual ~cA( void ){};
};

class cB : public cA
{
public:
    cB( void ){};
    ~cB( void ){};
};

void func( void )
{
    cA *pA = new cA();
    delete pA; // ok

    cB *pB = new cB();
    delete pB; // ok

    cA *pA = new cB();
    delete pA; // ok cB::~cB();
}

基底クラスと派生クラスで仮想関数(つまりデストラクタ)の名前が違っていますが、~cA( void ) が仮想になったのではなくて、あくまでデストラクタが仮想になったものと考えてください(デストラクタは一つしかありません)。基底クラスのポインタを介して派生クラスのオブジェクトにアクセスするようなプログラムでは、やはりそのポインタを使ってオブジェクトを解放する必要がある場合があり、そのようなときには仮想デストラクタが必須になります。


2, 純粋仮想関数

基底クラスがあまり抽象的で、関数を定義するのが困難な場合があります。例えば、次の cShape がそうです。

class cShape
{
public:
    virtual void Draw( void ){};
};

class cCircle : public cShape
{
public:
    void Draw( void ){};
};

class cTriangle : public cShape
{
public:
    void Draw( void ){};
};

Draw は画面に形を描画する関数だとします。cCircle では円を、cTriangle では三角形を描けばよいのですが、cShape だけでは何も描けません(「形」を描くとは?)。そこで、cShape::Draw を「純粋仮想関数」にします。

class cShape
{
public:
    virtual void Draw( void ) = 0;
};

メンバ関数の宣言の最後に = 0 を付加することにより、その関数は純粋仮想として宣言されます。あるメンバ関数が純粋仮想になると、次のことが起こります。

1, そのクラスは「抽象クラス」になる

純粋仮想関数を一つでも含むクラスは、すべて抽象クラスになります。抽象クラスのオブジェクトを作成することはできませんが、クラスのポインタと参照を宣言することはできます。

cShape shape; // error
cShape *pShape1 = new cShape(); // error
cShape *pShape2 = 0; // ok

2, 派生クラスで純粋仮想関数がオーバーライドされない場合、派生クラスも抽象クラスになる。

基本クラスにある純粋仮想関数のすべてがオーバーライドされて初めて、派生クラスのオブジェクトを作成することができます。このことから、現実的には、すべての派生クラスですべての純粋仮想関数をオーバーライドする義務が生じます。

つまり、純粋仮想関数を導入する目的は、すべての派生クラスにその関数をオーバーライドさせて、関数としての意味を持たせることにあります。

(補足:デストラクタを純粋仮想関数にすることもできますが、その場合の説明は割愛します)


これまで単一継承と、単一継承における仮想関数について説明しました。今後は、多重継承について説明したいと思っています。