コンストラクタは「自分自身を初期化する」だけではなく、「継承元やメンバ変数も初期化する」という大切な役割を持っています。でも、その関係をしっかり把握していないと、クラスの特性が変わってしまったりします。
今回は、この点についてしっかりと見ていきましょう。 |
コンストラクタからコンストラクタを呼び出す |
さて、コンストラクタが呼ばれたとき、主にすることはメンバ変数の初期化です。が、これについてはここまで解説していきませんでした。それは、メンバ変数がクラスの場合にはコンストラクタがコンストラクタを呼び出すことになるため、かなり複雑になるからです。
さらに、なんらかのクラスから継承しているばあい、その継承元のコンストラクタも呼び出す必要があります。つまりコンストラクタどうしが密接に関わり合っているわけです。 というわけで、ここからは「コンストラクタからコンストラクタをどう呼ぶか」について見ていこうと思います。 |
コンストラクタ初期化子 |
メンバ変数をいずれかの型の参照として持つ場合、次のような初期化はできません。
|
class CTest { public: // コンストラクタ。 CTest( int &p_ri ) { m_ri = p_ri; //コンパイルエラー。 } private: int &m_ri; }; void Test() { int i; CTest cTest( i ); }
これは、コンストラクタ内に入った時にすでに参照が初期化されているからです。実際には「コンパイルエラー」の行が無くてもエラーになります。
そこで出てくるのが「コンストラクタ初期化子」です。これは次のように使用します。 |
class CTest { public: // コンストラクタ。 CTest( int &p_ri ) : m_ri( p_ri ) //これがコンストラクタ初期化子です。 {} private: int &m_ri; }; void Test() { int i; CTest cTest( i ); }
コンストラクタ初期化子は、コンストラクタの関数名(通常「シグネイチャー」と呼びます)と実装部の間にコロンを挟んで置かれます。複数ある場合にはカンマで継なぎます。初期化には、必ず小カッコを使用します。これはデフォルトコンストラクタを呼び出すときでもです。逆に言うと、=は使用できません(ややこしい……)。
以上のような構文を使うことで、明示的に「どのコンストラクタを呼び出すか」を示すことができます。この構文を使わなければデフォルトコンストラクタが呼び出されるわけで、上のような「デフォルトコンストラクタが封印されている」場合にエラーが出る時には、必ずこの構文を使って呼び出すコンストラクタを指定する必要があります。 |
継承元クラスのコンストラクタの呼び出し |
コンストラクタ初期化子では、メンバ変数の他に、継承元クラスのコピーコンストラクタを呼び出すこともできます。
まず、コンストラクタが public 指定されていれさえすれば、継承先からどの引数のコンストラクタでも呼び出せます。この辺は普通にメンバ関数を呼び出すのと同じです。 |
class CTest0 //継承元。 { public: CTest0() { TRACE0( "CTest0::CTest0()\n" ); } private: //プライベート!! CTest0( int p_i ) { TRACE( "CTest0::CTest0( %d )\n", p_i ); } }; class CTest : public CTest0 { public: CTest() : CTest0() { TRACE0( "CTest::CTest()\n" ); } CTest( int p_i ) // : CTest0( p_i ) //コンパイルエラー。 : CTest0() //これはOK。どんな引数のでも呼び出せます。 { TRACE( "CTest::CTest( %d )\n", p_i ); } }; void Test() { CTest cTest1; CTest cTest2( 100 ); }
継承元クラスで呼び出せるのは、直接の親だけです。これは普通のメンバ関数の呼び出しとは違いますね。
ただし仮想継承は除きます。仮想継承は継承ツリーの中でひとつだけと決まっているので、複数のコンストラクタから呼び出されかねません。それを防ぐため、インスタンスの作成されたコンストラクタから直接呼び出せます(詳しい説明はナシ。いつか多重継承について解説する機会があれば……)。 |
初期化の順番 |
初期化には、明確な順番が存在します。
まず、各メンバ変数はクラス内での宣言の順番に初期化が行われます。決して「コンストラクタ初期化子の順番」ではありませんので注意してください。 |
class CMember { public: CMember( int p_i ) { TRACE( "CMember::CMember( %d )\n", p_i ); } }; class CTest { public: CTest() //この順番では ありません!! : m_cMember3( 3 ) , m_cMember1( 1 ) , m_cMember2( 2 ) {} private: //この順番で初期化されます。 CMember m_cMember1; CMember m_cMember2; CMember m_cMember3; }; void Test() { CTest cTest; // CMember::CMember( 1 ) // CMember::CMember( 2 ) // CMember::CMember( 3 ) }
継承元のクラスがある場合、継承元のコンストラクタが先、メンバ変数の初期化があとです。これもコンストラクタ初期化子には左右されません。
|
class CMember { public: CMember( int p_i ) { TRACE( "CMember::CMember( %d )\n", p_i ); } }; class CTest0 { public: CTest0() { TRACE0( "CTest0::CTest0()\n" ); } }; class CTest : public CTest0 //(1) { public: CTest() //以下の順番は無視されます!! : m_cMember( 0 ) , CTest0() {} private: CMember m_cMember; //(2) }; void Test() { CTest cTest; // CTest0::CTest0() //継承元が先、 // CMember::CMember( 0 ) //メンバ変数があと。 }
「継承元クラスがある場合」というのはすべてのコンストラクタでチェックされるので、結局はもっとも基底のクラスのメンバ変数の一番上が最初に呼ばれます。この基底クラスのメンバ変数がすべて初期化されたあと、コンストラクタの中身が実行されます。次にひとつ派生側のクラスのメンバ変数の一番上が初期化され、そのクラスのメンバ変数がすべて初期化されああと、コンストラクタの中身が実行され、次にまたひとつ派生側の……と続いて、最後にインスタンスの作成されたクラスのメンバ変数の一番上から順に初期化されて、最後にコンストラクタの中身が呼び出されるという順になります。
要は、基底クラスから作られるということです。ひとつのクラス型のインスタンスは、継承上にある各クラスのインスタンスがひとつに組合わさったものということがよく分かると思います。 |
どのコンストラクタを呼び出すか |
さて、以上で説明したようにコンストラクタは他のコンストラクタを自由に呼び出せるということが分かると思います。これは非常に重要なことです。
たとえば、あるひとつのクラスからふたつのクラスが public に派生しているとします。ヘッダーだけ見れば、基底クラスに関しては違いがないように見えます。ところが、コンストラクタ初期化子は実装部と一緒に書かなければならないため、上のようなインライン関数として作らない場合には、派生クラスが基底クラスのどのコンストラクタを呼び出しているのか分かりません。 これは、自分の作ったクラスが他の人が使ったクラスの継承元クラスになる場合に大きな問題になります。継承元クラスが「特殊な初期化」を必要とするにもかかわらず、そのクラスのコンストラクタのいくつかがその初期化を行わなかったらどうでしょう。このクラスのインスタンスを直接使用する場合には気を付ければある程度回避できる問題ですが、このクラスの派生クラスが「ちゃんとしたコンストラクタを呼んでいるか」確かめるすべはありません。 これには逆のケースもあり得ます。メンバ変数のひとつが「特定の引数を渡すコンストラクタを呼び出す必要がある」にも関わらず、初期化子を空白にしてデフォルトコンストラクタを呼び出してしまったとき、どのような誤動作が起きるか分かりません。 これらが重要な問題になるのは、コンストラクタが必ず呼び出さなければならないものだからです。何も指定しなければ、デフォルトコンストラクタが勝手に呼び出されます。そのために、あなたの作ったクラスが誤作動することになるかもしれません。クラスを作るときには必ず「どのコンストラクタを呼び出すべきか」ということを考えなければなりません。常にいくつかの中から最適なものを選び出さなければならないのです。 |
まとめ |
ここまで3回に渡ってコンストラクタについて解説してきました。が、もしかしたら読んできた方々の感想としては「で、何に使うの?」ということなのかもしれません。
実は、ここまで書いたことは特にする必要がないことだったりします。たとえばコンストラクタの private とかは、 public にして自分で気を付ければいいだけです。わざわざこだわって、整合性が付いていないためにコンパイルエラーが発生するというのは、面倒なことでしょう。 ですが、逆に言うと「コンパイルエラーが発生しやすいこと」が大きなメリットなのです。ここまで解説したことを実践し、厳しい特性を組み合わせた厳密な関係であれば、実行時エラーよりも先にコンパイルエラーが発生することでしょう。そういうことを望んでいる方には、今回の講座は結構だったんじゃないかなと思います。 |
(C)KAB-studio 1999 ALL RIGHTS RESERVED. |