コンストラクタはクラスの顔です。コンストラクタの設計がクラスの性格を決めると言っても過言ではありません。というわけで、コンストラクタの設計についてまとめてみましょう。
|
初期化と代入の違い |
まずは初歩的な話から。初期化と代入は違います。それを端的に表しているのが const です。
|
void Test() { const int i = 100; //これが初期化。 // i = 1000; //これが代入。コンパイルエラーになります。 int i1; int i2; int &ri = i1; //これが初期化。 // ri = i2; //これが代入。コンパイルエラーになります。 }
このように、 const だと初期化はできますが代入はできません。参照も(ポインタの const なので)参照先を初期化時に渡すことができますが、参照先を代入することはできません。
この違いは、クラスではさらに大きくなります。それは呼ばれる関数が違うからです。 |
class CTest { public: // コンストラクタ。 CTest( int p_i ) { TRACE( "CTest: %d\n", p_i ); } // 代入演算子。 CTest &operator = ( int p_i ) { TRACE( "operator =(): %d\n", p_i ); return *this; } }; void Test() { CTest cTest = 1; //これが初期化。 cTest = 100; //これが代入。 }
これを試せば、初期化時に CTest::CTest() 、つまりコンストラクタが、代入時に CTest::operator = () 、つまり代入演算子のオーバーロード関数が呼ばれていることが分かると思います。
これからの話は「初期化時」について説明していきます。 あと、初期化は次のような構文で呼び出せることを憶えておいてください。 |
class CTest { public: CTest( int p_i1, int p_i2 ) {} }; void Test() { int i( 100 ); //int i = 100; と同じ。 int i2(); //これはダメ。 // i2 = 200; //これがコンパイルエラーになります。 int i3 = int( 300 ); //こういう書き方もできます。 int i4( int( 400 ) ); //これもOK。 // void *pv( NULL ); //これはコンパイルエラー。 // (void *)pv( NULL ); //これはコンパイルエラー。 LPVOID lpv( NULL ); //これは通ります。 // int &ri( i ); //これはコンパイルエラー。 typedef int &intRef; //typedef すれば…… intRef ri( i ); //これはOK。 CTest cTest( 1000, 2000 ); //これは = では書けません。 }
C++では、初期化時に小カッコを使うことができます。これは=を使った初期化と同じです。ただし、引数ナシで呼び出そうとすると、変数宣言ではなく関数宣言とみなされてしまうのでできません(キャリコさんありがとう!!)
また、型名に直接小カッコを付けても初期化できます。このときには一時的な変数が作成されます。関数に直接渡す時などに使用される構文です。あと new を使用する場合にもこういった書き方をします。 小カッコを使用した初期化は、普通のポインタでは使用できません。小カッコの方が*よりも優先順位が高いからです。 LPVOID のように、 typedef してあれば小カッコで初期化できます。これは参照でも同じです。 また、クラスでは初期化時に2つ以上の引数を渡すことができます。このときには=では渡せないので、小カッコで初期化する必要があります。 以上の構文はこれからよく使いますので見慣れておいてください。 |
「デフォルト」と「コピー」 |
初期化には、値を渡さない場合と渡す場合のふたつがありました。
|
void Test() { int i1; //値を渡さない初期化。 int i2 = 100; //値を渡す初期化。 }
クラスでは、このふたつでは別々のコンストラクタが呼び出されます。「渡さない」方が「デフォルトコンストラクタ」、「渡す」方が「コピーコンストラクタ」です。
このふたつは、コードとして作成しなくてもコンパイラが勝手に作成してしまいます。 |
class CTest { public: // デフォルトコンストラクタとコピーコンストラクタはナシ。 void SetData( int p_i ) throw() { m_i = p_i; } int GetData() const throw() { return m_i; } private: int m_i; }; void Test() { CTest cTest; //デフォルトコンストラクタが呼び出されました。 cTest.SetData( 100 ); CTest cTest2 = cTest; //コピーコンストラクタが呼び出されました。 TRACE( "%d\n", cTest2.GetData() ); //100 }
コンパイラが作成したデフォルトコンストラクタは、メンバ変数を初期化しないという、そのままのものです。
コンパイラが作成したコピーコンストラクタは、メンバ変数を相手のメンバ変数ですべて初期化するというものです。 いまいちピンとこない? では上のコードを struct に置き換えてみてください。C++ではクラスと構造体はほとんど同じ物です。で、構造体について考えてみてください。 構造体型の変数を宣言する(=デフォルトコンストラクタが呼ばれる)とき、どのメンバも初期化されないでしょう。 また、同じ型の構造体型変数を渡して初期化する(=コピーコンストラクタが呼ばれる)と、各メンバがコピーされるでしょう。 これと同じ事がクラスにも言えるということですし、また、構造体にも「コンストラクタ」という概念があるということです。 さて、デフォルトコンストラクタとコピーコンストラクタが自動生成されることは分かりました。でも、通常はコンストラクタ内でメンバ変数を初期化したいでしょう。そういう場合には、これらのコンストラクタをちゃんとコードとして書くことになります。 |
class CTest { public: // デフォルトコンストラクタ。 CTest() { TRACE0( "デフォルト\n" ); } // コピーコンストラクタ。 CTest( const CTest &p_rcTest ) { TRACE0( "コピー( CTest )\n" ); } }; void Test() { CTest cTest1; //デフォルトコンストラクタ。 CTest cTest2( cTest1 ); //コピーコンストラクタ。 }
デフォルトコンストラクタは引数を取りません。
また、コピーコンストラクタは「const 自分型」の参照を引数に取ります。 const なのは、そうでないと「const 自分型」を渡せないからです。あと必ず「参照」である必要があります(そうでないと無限にコンストラクタが呼ばれることになるので)。 また、デフォルトコンストラクタは次のようにすることもできます。 |
class CTest { public: // デフォルトコンストラクタも兼ねています。 CTest( int p_i1 = 0 ) { TRACE( "コピー( int )\n" ); } };
このように「引数を省略できるコンストラクタはデフォルトコンストラクタにもなる」というわけです。これを使用すべきかどうかというのは「引数の省略をすべきか関数のオーバーロードをすべきか」ということの問題になるのでここでは解説しません。
|
つづく |
さて、ここまでは単なる構文解説です。ただの基礎知識です。
そして次の「後編」では、以上に基づいたさらに突っ込んだ「オブジェクト指向」について解説していきたいと思います。 |
(C)KAB-studio 1999 ALL RIGHTS RESERVED. |