前回の問題点を解決しましょう。
問題点は「ポインタ変数を持ったクラスの代入」にありました。
<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するのもコピーです。
「=」演算子を使わないから、コピーコンストラクタを使わないからと言って対応していないと深刻なバグが発生します。
次回は、コピー自体を禁止する方法を説明します。