★安全な代入★


前回の問題点を解決しましょう。

問題点は「ポインタ変数を持ったクラスの代入」にありました。

<sample program cpp069-02>

#include <iostream>

class Stage {
public:

    static const int FIELD_SIZE;

    Stage();

    ~Stage();

    bool Initialize();

private:

    int *m_pField;
};

int main()
{
    Stage stage1;

    if (!stage1.Initialize()) {
        return 1;
    }

    Stage stage2;

    stage2 = stage1; //ここが問題!

    return 0;
}

const int Stage::FIELD_SIZE = 10;

Stage::Stage() : m_pField(NULL)
{

}

Stage::~Stage()
{
    if (m_pField) {
        delete[] m_pField;
        m_pField = NULL;
    }
}

bool Stage::Initialize()
{
    m_pField = new int[FIELD_SIZE];

    if (!m_pField) {
        return false;
    }

    return true;
}

単純に代入すると、確保した領域のアドレスがコピーされ、デストラクタ関数で同じ領域を2回解放してしまいます。


安全な代入


そこで、この「=」演算子をオーバーロードし、安全にコピー出来るようにしてみます。

<sample program cpp070-01>

#include <iostream>

class Stage {
public:

    static const int FIELD_SIZE;

    Stage();

    ~Stage();

    bool Initialize();

    Stage& operator=(const Stage &rhs);

private:

    int *m_pField;
};

int main()
{
    Stage stage1;

    if (!stage1.Initialize()) {
        return 1;
    }

    Stage stage2;

    stage2 = stage1;

    return 0;
}

const int Stage::FIELD_SIZE = 10;

Stage::Stage() : m_pField(NULL)
{

}

Stage::~Stage()
{
    if (m_pField) {
        delete[] m_pField;
        m_pField = NULL;
    }
}

bool Stage::Initialize()
{
    m_pField = new int[FIELD_SIZE];

    if (!m_pField) {
        return false;
    }

    return true;
}

Stage& Stage::operator=(const Stage &rhs)
{
    if (rhs.m_pField) {

        if(Initialize()) {

            for (int i = 0; i < FIELD_SIZE; i++) {
                m_pField[i] = rhs.m_pField[i];
            }
        }
    }

    return *this;
}

<実行結果>

続行するには何かキーを押してください・・・

実行しても特に何も起こりませんが、正常に終了出来ます。


「=」演算子のオーバーロード関数を確認します。

Stage& Stage::operator=(const Stage &rhs)
{
    if (rhs.m_pField) {

        if(Initialize()) {

            for (int i = 0; i < FIELD_SIZE; i++) {
                m_pField[i] = rhs.m_pField[i];
            }
        }
    }

    return *this;
}

最初のif文で、引数となっている右辺のポインタ変数に領域が割り当てられているかどうか調べます。

m_pFieldはコンストラクタ関数で初期値「NULL」が入っているので、領域が割り当てられていなければこのif文は成り立ちません。

右辺のポインタ変数に領域が割り当てられていたら、Initialize関数を呼び出し、自分自身も同じサイズの領域を確保します。

確保出来たら、右辺の配列の中身を自分自身の領域にコピーします。

確保できなければ、m_pFieldは「NULL」になります。


これなら2つのポインタは別々の領域を指す事になりますので安全です。

このように代入演算子のオーバーロードを作る事で、危険なコピーを回避する事が出来るのです。


しかし、これで安心してはいけません。

クラスのコピーは代入演算子だけの機能では無く「コピーコンストラクタ」によっても発生します。

と言う事で、コピーコンストラクタへの対応も追加します。

<sample program cpp070-02>

#include <iostream>

class Stage {
public:

    static const int FIELD_SIZE;

    Stage();

    Stage(const Stage &rhs);

    ~Stage();

    bool Initialize();

    Stage& operator=(const Stage &rhs);

private:

    int *m_pField;
};

int main()
{
    Stage stage1;

    if (!stage1.Initialize()) {
        return 1;
    }

    Stage stage2;

    stage2 = stage1;

    Stage stage3(stage2);

    return 0;
}

const int Stage::FIELD_SIZE = 10;

Stage::Stage() : m_pField(NULL)
{

}

Stage::Stage(const Stage &rhs)
{
    *this = rhs;
}

Stage::~Stage()
{
    if (m_pField) {
        delete[] m_pField;
        m_pField = NULL;
    }
}

bool Stage::Initialize()
{
    m_pField = new int[FIELD_SIZE];

    if (!m_pField) {
        return false;
    }

    return true;
}

Stage& Stage::operator=(const Stage &rhs)
{
    if (rhs.m_pField) {

        if(Initialize()) {

            for (int i = 0; i < FIELD_SIZE; i++) {
                m_pField[i] = rhs.m_pField[i];
            }
        }
    }

    return *this;
}

<実行結果>

続行するには何かキーを押してください・・・

実行しても特に何も起こりませんが、正常に終了出来ます。


コピーコンストラクタの宣言は、

Stage(const Stage &rhs);

引数にそのクラスの「const参照」を受け取るコンストラクタ関数です。

本体は、すでに代入演算子のオーバーロードを作っていますので、

Stage::Stage(const Stage &rhs)
{
    *this = rhs;
}

安全に代入するだけにしました。

コピーコンストラクタの呼び出し方は、

Stage stage3(stage2);

のように、クラスの実体を作る際に引数として別の実体を渡す事で呼び出されます。


これで2種類のコピーに対応出来ました。

本当に機能しているかどうか、最後に確かめてみましょう。

<sample program cpp070-03>

#include <iostream>

class Stage {
public:

    static const int FIELD_SIZE;

    Stage();

    Stage(const Stage &rhs);

    ~Stage();

    bool Initialize();

    Stage& operator=(const Stage &rhs);

private:

    int *m_pField;
};

int main()
{
    Stage stage1;

    if (!stage1.Initialize()) {
        return 1;
    }

    Stage stage2;

    stage2 = stage1;

    Stage stage3(stage2);

    return 0;
}

const int Stage::FIELD_SIZE = 10;

Stage::Stage() : m_pField(NULL)
{

}

Stage::Stage(const Stage &rhs)
{
    *this = rhs;
}

Stage::~Stage()
{
    if (m_pField) {

        std::cout << "delete -> " << m_pField << std::endl;

        delete[] m_pField;
        m_pField = NULL;
    }
}

bool Stage::Initialize()
{
    m_pField = new int[FIELD_SIZE];

    std::cout << "new -> " << m_pField << std::endl;

    if (!m_pField) {
        return false;
    }

    return true;
}

Stage& Stage::operator=(const Stage &rhs)
{
    std::cout << "use operator =" << std::endl;

    if (rhs.m_pField) {

        if(Initialize()) {

            for (int i = 0; i < FIELD_SIZE; i++) {
                m_pField[i] = rhs.m_pField[i];
            }
        }
    }

    return *this;
}

<実行結果>

new -> 002DF048
use operator =
new -> 002DA570
use operator =
new -> 002DA008
delete -> 002DA008
delete -> 002DA570
delete -> 002DF048
続行するには何かキーを押してください・・・

newで領域を確保した時、deleteで領域を解放した時にアドレスを表示しました。

また、代入演算子のオーバーロードが呼び出された時にもメッセージを表示しています。

実行結果に説明を付けると、

new -> 002DF048    stage1の領域確保
use operator =      stage2へ代入
new -> 002DA570    stage2の領域確保
use operator =      stage3のコピーコンストラクタ呼び出し
new -> 002DA008    stage3の領域確保
delete -> 002DA008 stage3の解放
delete -> 002DA570 stage2の解放
delete -> 002DF048 stage1の解放

のようになります。


最後に・・・


実際には、関数の引数へ値渡しをするのもコピーですし、コンテナにpushするのもコピーです。

「=」演算子を使わないから、コピーコンストラクタを使わないからと言って対応していないと深刻なバグが発生します。

次回は、コピー自体を禁止する方法を説明します。


次へ

戻る

目次へ