こんにちは、みにくい社長です。
今日はオブジェクト指向プログラミングの基礎、コンストラクタについて書きます。
初期化方法
コンストラクタはオブジェクト作成時に必ず呼ばれる初期化用関数です。 対になるものにデストラクタがあり、オブジェクトが破棄される際に必ず呼ばれます。 C++ではオブジェクト作成時のメンバ変数の値が0にクリアされる保証が無い為、初期化を手動で記述する必要があります。 下記のように、いろいろな初期化方法があります。
class Parent { public: // コンストラクタ Parent() : _valueC(1), _valueA(2){ _valueB = 3; } // デストラクタ ~Parent(){} private: int _valueA; int _valueB; int _valueC; int _valueD = 3; }
この時、valueA, valueC, valueDは変数が作られる時に初期化され、valueBは変数を作成後に値が代入されます。 微妙な差ではありますが、変数が作られる際に初期化をする方が高速なので、理由が無ければそのように記述することをお勧めします。 valueDの初期化の記述方法について、Visual Studioのコンパイラはこの記述を許容できますが、他のコンパイルではエラーになる可能性がありますのでお勧めしません。 また、valueAとvalueCのメンバ変数の記述順序とコンストラクタの初期化順序が逆になっています。 このような記述は速度低下を招くことがあり、コンパイラによってはwarning扱いになりますので避けましょう。
呼び出し順序
クラスが継承されている時、コンストラクタとデストラクタが呼ばれる順序について考えてみましょう。
class Parent { public: // コンストラクタ Parent(){ initialize(); } ・・・① // デストラクタ ~Parent(){} ・・・④ protected: // 初期化関数 virtual void initialize(){ _valueA = 3; } ・・・①-② int _valueA; } class Child : public Parent { public: // コンストラクタ Child(){} ・・・② // デストラクタ ~Child(){} ・・・③ protected: // 初期化関数 void initialize() override { Parent::initialize(); _valueB = 2; } ①-① int _valueB; }
継承したChildクラスを生成および破棄した場合、コンストラクタは①②の順、デストラクタは③④の順に呼ばれます。 つまり生成は親が先に呼ばれ、破棄は子が先に呼ばれます。 この時に注意が必要なのが、「コンストラクタ及びデストラクタでは仮想関数を呼ぶと不正アクセスになることがある」ということです。 ①のコンストラクタからinitialize仮想関数が呼ばれていますが、overrideしている①-①が呼ばれることになります。 ①-①でvalueBにアクセスしていますが、まだ②のコンストラクタが呼ばれていないため、初期化前のvalueBにアクセスしてしまいます。 このような不正アクセスは予想外の挙動になりますので、コンストラクタから仮想関数を呼ぶのは避けましょう。 回避策としては、initialze, terminateのような初期化、解放処理用の関数を別途用意し、仮想関数はそれらから呼び出すようにしましょう。
コンストラクタの応用
コンストラクタは必ず呼び出される為、それを応用して実現できることもあります。 OROCHIエンジンでは初期値と異なる値を変更済みと判定し、初期値に戻す操作が可能になっています。 コンストラクタを呼び出した直後のオブジェクトの値を初期値とし、その初期値オブジェクトと比較することで差分を検出しています。
class Parent { public: // コンストラクタ Parent() : _valueA(2) { } protected: // 初期値判定 bool isDefault(){ _valueA == _defaultObject._valueA; } int _valueA; static Parent _defaultObject; }
コンストラクタはオブジェクト指向プログラミングの基礎です。正しい知識でプログラミングに臨みましょう。 では、またねー。