1-3 コンストラクタとデストラクタ

C++で重要なものとしてコンストラクタとデストラクタというものがあります。
簡単に説明すると、クラスの宣言時に実行される関数をコンストラクタ
クラスを破棄するときに実行される関数をデストラクタと言います。
そして、これらの関数は自動的に実行するようにコンパイルされます。


■概要

とりあえず以下のクラスを見てください。

class CTest {
    int  iData;
    char *pData;
public:
    CTest();                    // コンストラクタ
    ~CTest();                   // デストラクタ
    BOOL SetData( int data );
    int GetData( void );
};

赤字で書いているところがコンストラクタとデストラクタの定義ですが、よく見ると決まった書式があります。

まず、クラスを定義するときに指定した名前(ここではCTest)がコンストラクタ、
この名前に
~(チルダ/個人的にはにょろまーく)がはじめについたものがデストラクタになります。
これはシフトを押しながら「へ」を押すことで入力出来ます。

さて、まずはなぜコンストラクタが必要になるかというと、クラスの中に定義したiDataというものは、
SetData()メソッドを使わない限り何が入っているか分からない状態となっています。
これはC言語は確保された直後に明示的に0クリアはされず、以前使用していた変数などのゴミが残ったままとなっているためです。

もしここでSetData()より先にGetData()を呼び出してしまった場合、その値は何が入っているか分からない状態になっており、
このようなプログラムではある時は問題なく動作するのに、ある時は問題が発生するといった不明なバグが発生します。

これを避けるにはコンストラクタと呼ばれる関数にその初期化を書いておきます。
すると、クラスが作られた時に自動的にこのコンストラクタが呼ばれるため、確実に変数の初期化を行うことが出来ます。

次にデストラクタですが、これはクラスを破棄するときに自動的に呼ばれる関数です。

デストラクタの利点を簡単に説明すると、例えば普通に下のようにpDataのポインタにメモリを確保していたとします。

void Test()
{
    char *pData = (char *)malloc( 1000 );   // メモリ確保

    if( <エラー発生> ) {
        // エラーのため関数を抜ける
        return;
    }

    free( pData );                          // メモリ開放
}
通常はメモリを確保したら必ず開放が必要ですが、途中でエラーが発生した場合は最後のfreeが呼び出されずこのメモリバッファはどこかに行ってしまいます。

このためデストラクタ内で開放処理を書いておけば、どのタイミングでクラスが終了されようとしても、
必ずこのデストラクタが実行されるため自動的に開放する事が出来るわけです。


■定義方法

コンストラクタとデストラクタの関数の書き方は通常のメソッドと変わりありません。
コードは以下のように記述します。

// コンストラクタの処理
CTest::CTest()
{
    iData = 0;
    pData = NULL;
}

// デストラクタの処理
CTest::~CTest()
{
    if( pData ) {
        free( pData );
    }
}

これで実体を確保したと同時に初期化が行われ、実体が破棄されるときにpDataにメモリが確保されていた場合は開放するプログラムになります。

ちなみにコンストラクタとデストラクタには引数や戻り値が無いように見えますが、実はコンストラクタだけは引数を設定することが出来ます。
また戻り値は存在しないため、常にvoid型となるので通常は省略されます。


■実体の確保・破棄(開放)

上のほうで実体の確保や破棄などと書いていますが、これは実際どういうことなのかというと、
確保とは実体を宣言したとき、破棄は実体が無くなる時を言います。

ここではこれを詳しく説明しますが、まずはクラスの定義方法には2種類存在します。

記述例 定義名称 内容 備考
CTest ct; 静的確保 直接変数として定義する方法 ローカル変数の場合はスタック、グローバル変数ではデータセグメントに配置
CTest *pct = new CTest; 動的確保 newを使用して実体を確保する方法 任意の空きメモリ空間に配置

通常は1の静的確保を使いますが、サンプルとしてまず以下のプログラムを見てください。

CTest ct;   // 実体をグローバルに確保

void Sample( void )
{
    printf( "%d",ct.GetData() );
    return;
}

この例ではグローバル変数として定義しているので、main()関数が始まる前に実体が確保され、そのときにコンストラクタで初期化が行われます。

そして、Sample()関数内で自由に値を参照したりセットしたり出来ますが、プログラム自体が終了しなければCTestは常に実体を持ったままの状態になります。

次に以下のプログラムを見てください。

void Sample( void )
{
    CTest ct;   // 実体を関数内に確保

    printf( "%d",ct.GetData() );
    return;
}

この場合は、Sample関数が呼ばれたときに実体が確保され、この時にコンストラクタが呼ばれ初期化されます。

重要なのは、このSample関数がreturnで抜ける時にデストラクタが呼ばれることです。
デストラクタが呼ばれるタイミングはちょうどreturnの手前になりますが、これはコンパイラが勝手に行います。
これをクラスの破棄と言います。

もし関数の途中でreturnを使用しても、必ずその手前で破棄が行われます。


実はこれらの挙動は一般的なローカル変数と同じような扱いとなります。
例えば関数内でint i;と宣言した場合、それは関数内のみでしか使用できないのと同じです。
同じくグローバルでint i;と宣言した場合は、どの関数内からでも使うことが出来ます。


ちなみに、当然ですがクラスも配列で宣言することも出来ます。
CTest ct[10]; とすると、10個分のCTestクラスが確保されます。
そして同時に10個分のコンストラクタが呼ばれます。
またアクセス方法も通常の配列変数と同様に、ct[i].GetData(); というように書くことが出来ます。
ただし、欠点はこの場合はコンストラクタに引数を持たせることが出来ません。


■newで実体の確保

ここではもう1つのnewによる動的確保について説明します。

以下のプログラムを見てください。

void Sample( void )
{
    CTest *ct;                      // ポインタを宣言

    ct = new CTest();               // 確保

    printf( "%d",ct->GetData() );

    delete ct;                      // 破棄

    return;
}

関数内でポインタを宣言していますが、ここではまだ実体を確保したことにはなりません。

次にnewを使いこのポインタにクラスの実体を作成しています。
この時同時にCTestのコンストラクタが呼ばれ値が初期化されますが、これが実体を確保した状態となります。
ちなみに、引数の無いコンストラクタの場合は括弧を省略して「new CTest;」と書くことが出来ます。

表示を行う部分ではctはポインタのため、「->」を使用してメソッドを実行しています。

そして最後にdeleteを呼び出していますが、ここでデストラクタが実行され実体を開放した状態となります。
もしdeleteを行ったあと再びctに対して何か処理を行うと、既に実体が存在しないためプログラムがクラッシュすることになります。


なお、基礎アルゴリズムの動的メモリ確保でも説明していますが、newとdeleteは1対1で使用します。
つまりnewを行うとコンストラクタが、deleteを行うとデストラクタが呼ばれるので、これらはひとまとまりで使うということが分かると思います。

一番重要なのはnewで確保したものはいくらreturnをしても開放はされないので、この方法を使うにはかなり注意する必要があります。

また、ここではctを関数内に確保していますが当然グローバル変数として定義することも出来ます。
こうすることで静的確保と同じように、プログラムのいろいろなところから参照出来るようになりますが、
この場合はきちんと実体を確保してからアクセスしないとプログラムがクラッシュしてしまいます。


■コンストラクタで引数を宣言

ここまででクラスの概要はほぼ説明しましたが、さらにおまけとしてコンストラクタで引数を指定するクラスを説明します。

まず以下のクラスを見てください。

class CTest {
    int iData;
public:
    CTest( void );
    CTest( int data );    // 引数のあるコンストラクタを追加
}

コンストラクタが2つ定義されていますが、C++では引数が違えば同名の関数を宣言することが出来ます。
つまり引数が違うと別の関数としてコンパイラが認識し、呼び出し時に与えられた引数の型によって、
自動的に対応する関数が呼ばれためこのような書き方も許されます。

さて、2つ目の引数のあるコンストラクタですが、これの実体は以下のようになっているとします。

CTest::CTest( int data )
{
    iData = data;
}

もう分かるかもしれませんが、ここではコンストラクタでデフォルト値を設定しています。
つまり、実体の確保時に同時に値をセットすることが出来るということです。

それでは実際にデフォルト値をセットした状態で実体を確保するプログラムを紹介します。
以下が直接実体を定義した場合です。

void Sample( void )
{
    CTest ct( 256 );
    printf( "%d",ct->GetData() );
}

実体を定義するのと同時に、関数みたいに括弧を使用して規定値を与えています。


そして以下がnewを使用した場合です。

void Sample( void )
{
    CTest *ct;

    ct = new CTest( 256 );

    printf( "%d",ct->GetData() );

    delete ct;
}

newで実体を確保するときに括弧を使い規定値を与えています。


これらの例では256と定数を入れていますが変数も代入することが出来ます。
また引数は1つだけとは限らず、当然関数のようにいくつも指定することが出来るので、
用途に応じてコンストラクタをたくさん用意することも出来ます。


■デストラクタのおまじない

以下の内容は古いC++のコンパイラで問題が発生するもので、新しいコンパイラではこの問題は解決されています。
古いコンパイラを使うことがあるなど、必要ならば念のため確認しておいてください。



このマニュアルで説明しているクラスは、どれも直接そのクラスを指定して実体を作るので、
あまり関係ないとも言えますが、ここではデストラクタについて少しだけ問題点を洗い出してみます。
ちなみに、これからプログラマーになろうと思っている人は、これは確実に覚えておいたほうがいいです。

クラスはそれを元としてさらに新しいクラスを作ることが出来ます。
これを継承と言いますが、この継承時にもとの継承前のクラスのデストラクタが呼び出されないという問題があります。

たとえばまず以下のように元となるクラスを定義します。

// サンプルメモリクラス
class CMemory {
protected:
    char *pBuf;           // メモリバッファ
    int iData;            // メモリサイズ
public:
    CMemory();                           // コンストラクタ
    ~CMemory();                          // デストラクタ
    void Add( char *data,int size );     // バッファにデータを追加
};

そしてこれを継承します。

// 継承文字列クラス
class CString : public CMemory {
public:
    CString();
    ~CString();
    char *GetString( void );
};

そしてプログラム中で使ってみます。

      :
    // 文字列クラスを構築
    CString *s = new CString();
      :
    s->Add( "文字列だにょ",12 );
    s->Add( NULL,1 );
    printf( "%s¥n",s->GetString() );
      :
    // 破棄
    delete s;
      :

一見なんの変哲も無いプログラムですが、実はこれではメモリリークが発生してしまいます。

なぜかと言うと、破棄を行う際にCStringのデストラクタは呼び出されますが、
継承元のCMemoryのデストラクタが呼び出されないためです。
これはコンパイラがそのようになっているためで仕様です。

そこで、継承元のクラスにはおまじないをしておきます。
それがvirtual指定です。

// サンプルメモリクラス
class CMemory {
protected:
    char *pBuf;           // メモリバッファ
    int iData;            // メモリサイズ
public:
    CMemory();                           // コンストラクタ
    virtual ~CMemory();                  // デストラクタ
    void Add( char *data,int size );     // バッファにデータを追加
};

本来は仮想関数を作るという目的のvirtualですが、デストラクタにこれを適用することで、
必ずそのクラスのデストラクタが呼び出されるようになります。

ちなみに、virtual指定は上位に1回でも定義されていれば、それから派生したクラスは暗黙的にvirtual指定されるので、
CStringのデストラクタにはvirtual指定をしなくても問題ありません。

しかし、このCStringもいつかCMemoryの派生から抜けたり、また逆にCStringも今度は自分が派生元のクラスとなりえるかもしれないので、
やはりvirtual指定を付けて置いたほうが安全です。

ということで長々と説明してきましたが、ようは何が言いたいかというとすべてのデストラクタにはvirtualを定義すべしということです。