オブジェクト間の関係をどのように記述するかを述べていきます。
0. 目次
1. オブジェクト間の関係概論
オブジェクト指向言語で悩ましくなるのが、クラス定義で内容を区切ってインターフェース公開を行ったまでは良いが、実際にはそのオブジェクト間でインターフェース呼び出しを行いたくなる点です。オブジェクトの生存期間が局所的である場合は、インターフェースの内部で宣言して動作させれば良いわけですが、そのようなクラスライブラリ的なオブジェクトを設計することはほとんどないでしょう。実際には生存期間の長いクラス同士でインターフェース呼び出しが出来ないと、全部が全部 main 関数に書くことになり、大抵上手く設計できません。このようにオブジェクトがオブジェクトを呼び出すような動きをどのようにクラスで定義するのか、以下にガイドラインを示します。
2. 引数参照
2.1. 基本事項
各インターフェースごとに、必要に応じてオブジェクトへの参照を渡すやり方です。ポインタを用いる場合と参照型を活用する場合とあります。なお、参照型(&) を用いてもポリモーフィズムは実現されますので、派生系クラスのオブジェクト呼び出しも可能です。 現段階で特に規約はありませんが、NULL チェックを行わなくて良い限りにおいて、参照型を用いる方がソースの可読性は良くなるかも知れません。
class CSample
{
public:
CSample(){}
virtual ~CSample(){}
void function( CBook& book )
{
book.Disp();
}
void function( CBook* pBook )
{
pBook->Disp();
}
};
2.2. 良くない例
以下のようなオブジェクト自体を渡すのは基本的にはよろしくありません。
class CSample
{
public:
CSample(){}
virtual ~CSample(){}
void function( CBook book )
{
book.Disp();
}
};
このように行うと、function 呼び出し時、CBook のオブジェクトが引数として『渡したオブジェクトとは別に』作成され、オブジェクト内のメンバ変数をコピーする、と言う動作となってしまいます。メモリ効率の点でも実行速度の面でもよくありませんし、そもそも引数として渡したオブジェクトの属性は変化することがありません。変化するのは引数としてコピーされた方のオブジェクトだからです。この挙動はC言語の引数でも同様ですね。
2.3. 上級技
今回、CBook::Disp() はメンバ変数に対して読み込みだけを行い、書き込みはありませんから、const 属性のメンバ関数とすることが出来ます。const 属性のメンバ関数については、『クラスとオブジェクト 2.2.2.1. 基本的なメンバ関数を実装』にて解説していますので、参照してみてください。
// 書籍クラス
class CBook
{
protected:
… 省略 …
public:
… 省略 …
// -----------------------------------
// Summary:
// 状態の表示
virtual void Disp() const
{
cout << m_pszTitle << "のデータ" << endl;
cout << "--------------------------" << endl;
cout << "値段:" << m_nPrice << endl;
cout << "入荷した冊数:" << m_nArrivedValue << endl;
cout << "販売した冊数:" << m_nSelledValue << endl;
cout << "売り上げ:" << GetSells() << endl;
cout << "残冊数:" << GetRestValue() << endl;
}
};
このようにしておく意味は『オブジェクトの属性に変化はありませんよ』と言う意味です。すると、以下のように引数参照にも const を指定することができるようになります。
class CSample
{
public:
CSample(){}
virtual ~CSample(){}
void function( const CBook& book )
{
book.Disp();
}
void function( const CBook* pBook )
{
pBook->Disp();
}
};
const の指定はライブラリとしての完成度を上げる意味で大切ですが、現在これに関する規約はありません。ので、上級技です。且つ、注意事項があります。今回作成した void Disp() const は、このスタイルそのものが関数の「型」になってしまいますため、オーバーライドする場合は、継承クラス全てに渡って void Disp() const と const な関数であることを定義しなければなりません。
3. メンバ変数としての参照
頻繁に関連クラスのオブジェクトを用いる場合、そのオブジェクトの参照ないしはポインタをメンバ変数として持つ方法があります。必ず初期化しなければならない場合は参照型メンバ変数、NULL (オブジェクトが存在しない)場合が存在する場合はポインタを使うと言う実装が良いでしょう。これも特に規約があるわけではありません。
// 参照型を用いる例
class CSample
{
protected:
CBook& m_Book;
public:
CSample( CBook& Book )
:m_Book( Book )
{}
virtual ~CSample(){}
void function()
{
m_Book.Selled();
m_Book.Disp();
}
};
// ポインタを用いる例
class CSample2
{
protected:
CBook* const m_pBook;
public:
CSample2( CBook* pBook = NULL )
:m_pBook( pBook )
{}
virtual ~CSample2(){}
void function()
{
// NULL である可能性があるため、チェックが必要
if( m_pBook ){
m_pBook->Selled();
m_pBook->Disp();
}
}
};
ポインタを用いる例の CSample2 メンバ変数 CBook* const m_pBook における、const は、このポインタ変数が変更されないことを示すちょっとした拘りです。Effective C++ 第4章21項『使えるときは、必ず const を使おう』を参照してみると良いでしょう。これも特に規約があるわけではないので参考程度です。
CBook* const m_pBook; // ポインタ自体は const
// オブジェクトは const ではない
const CBook* m_pBook; // ポインタ自体は const ではない
// オブジェクトは const
//( const メンバ関数以外は操作できない)
const CBook* const m_pBook; // ポインタ自体は const
// オブジェクトも const
//( const メンバ関数以外は操作できない)
3.1. 使用例
以下、オブジェクトの使用例と実行結果を示します。コードは CSample / CSample2 を用いて CGeneralBook と CMagaine のオブジェクトを操作しています。操作が確かにコンストラクタで渡されたオブジェクトに対して行われていることを確認するために、各々のオブジェクトに対して Disp() メンバ関数を呼び出しています。
int main()
{
CGeneralBook HarryPotter( "ハリーポッター賢者の石",
"J.K.ローリング",1900, 52, 0 );
CMagazine Gendai( "月間現代",
780, 8, 0, CMagazine::Monthly, 10 );
CSample Sample( HarryPotter );
Sample.function();
CSample2 Sample2( &Gendai );
Sample2.function();
cout << "==================================== 実験 ";
cout << endl << endl << endl;
HarryPotter.Disp();
Gendai.Disp();
return -1;
}
実行結果は下記通りです。function() メンバ関数の中で行われている CBook::Selled() 呼び出しが反映されている点、CBook::Disp() のポリモーフィズムが動作して CGeneralBook::Disp() 、 CMagazine::Disp() が呼び出されている点に注目ください。

4. 包含
クラス定義の中でオブジェクト自体をメンバ変数で持つ方法があります。これを包含(アグリゲーション)と言ったりします。この実装の欠点は各メンバオブジェクトのメンバ関数にアクセスするためにはその都度それを持つクラスにメンバ関数を追加しなければならない点です。MFC の文字列操作クラスである CString と言った属性に取って代わることの出来るクラスのオブジェクトなどに用いるのが良いでしょう。
// 包含の例
class CSample3
{
protected:
CMagazine m_Gendai;
public:
CSample3()
:m_Gendai( "月間現代", 780, 8, 0, CMagazine::Monthly, 10 )
{}
~CSample3();
void function()
{
m_Gendai.Selled();
m_Gendai.Disp();
}
};
包含されるオブジェクトは、包含するクラスのコンストラクタで変数同様な初期化処理が必要である点に注意ください。
5. 補足事項
5.1. 関連を示すクラス定義の注意事項
参照・包含されるクラスは、前もって定義されている必要があります。特に『クラスとオブジェクト 4.3. 定義ファイル、実装ファイル』を用いる場合、2重定義の罠にはまることがしばしばあります。
現在工事中:
6. 演習問題
『ポリモーフィズム 5. 演習問題』で作成したプログラムを、各商品オブジェクトのリストを扱うような包含ないしは参照関係にあるクラスを検討し改変してください。