ページの末尾へ トップページ > ヘッダファイルの作り方(その 1) > ヘッダファイルの作り方(その 2)
TOPIC 節電しましょう。電力の使用状況グラフ(東電)
ここでは、二つのヘッダファイルが互いにインクルードしあう場合について解説します。
初めから出鼻をくじくような話になるのですが、実は、テンプレートクラスを含むヘッダファイル同士は、呼び合う必要がありません(=なのでこのタイトルは間違っているわけですね(笑))。どうしてもヘッダファイル同士が呼び合わなくてはならないのは非テンプレートクラスを含むヘッダファイルの場合なのですが、初めからこれを解説しても、何を言っているのか分からないということになりかねないので、まずは、簡単な「テンプレートクラスを含むヘッダファイル」から初めましょう。
通常のクラスよりも、テンプレートクラスの方がそれ自体は要素が多くて難しいのですが、「ヘッダ同士が呼び合う必要があるかどうか」という点に関しては、コンパイラがソースコードをどのように扱うのかという問題なので、これに限っては「テンプレートクラスを扱うヘッダファイル」の方がずっと簡単なのです。
さて、前振りはこのぐらいにして、早速コードサンプルを示すことにします。ここでは、まだヘッダファイルは使いません。
<Main.cpp> の中 template <class T> class CClassA { public: void FunctionA() { CClassB<int> classbi; // A の中で B を使う。 classbi.FunctionB(); } }; template <class T> class CClassB { public: void FunctionB() { CClassA<int> classai; // B の中で A を使う。 classai.FunctionA(); } }; int main() { CClassA<int> a; CClassB<int> b; a.FunctionA(); b.FunctionB(); return 0; }
上記ソースコードの main() の中では、CClassA と CClassB のオブジェクト a, b を作成し、それぞれのメソッド a.FunctionA()、b.FunctionB() を呼び出しています。CClassA::FunctionA() の中では、CClassB(=相手クラス)のオブジェクトを作成して、そのメソッドを呼び出しています。逆に CClassB::FunctionB() の中では、CClassA(=これも相手クラス)のオブジェクトを作成して、そのメソッドを呼び出しています。
要するに、A は B を使い、B は A を使うという関係にあるわけです。ちなみに、上記コードを実行すると、A が B を呼び、次に B が A を呼び、次に A が B を呼び,...となって、おそらくスタックオーバーフローでクラッシュしますが(オイオイ)、ここでは、実際の動作は問題ではなく、解説しようとしているのは、上記の例のように互いに呼び合うクラスを定義する場合、ソースコードをどのようにファイルに分けて配置し、どのような事を配慮すればうまくコンパイル出来るようになるのかということですので、そのような目でソースコードを見てください。
上記サンプルコードは、そのままで何の問題もなくコンパイルできて、「めでたしめでたし」です。
ただ、上記は、ひとつの Main.cpp ファイルに記述されているわけで、クラスをヘッダに分けて記述するというようにはなっていません。そこで、テーマに従ってこれらをヘッダファイルと cpp ファイルに分けてみると、次のようになります。
<A.h> の中 #pragma onece template <class T> class CClassA { public: void FunctionA() { CClassB<int> classbi; // A の中で B を使う。 classbi.FunctionB(); } }; <B.h> の中 #pragma onece template <class T> class CClassB { public: void FunctionB() { CClassA<int> classai; // B の中で A を使う。 classai.FunctionA(); } }; <Main.cpp> の中 #include "A.h" #include "B.h" int main() { CClassA<int> a; CClassB<int> b; a.FunctionA(); b.FunctionB(); return 0; }
これは、ヘッダファイルにインクルードガードの #pragma onece が入っただけで、ヘッダファイル A.h と B.h は互いに相手のファイルを #include で読み込んではいないことに注意してください。このようにテンプレートクラスの場合、例えば A.h で定義されている CClassA の中で、A.h の内部には全く定義がない CClassB のオブジェクトを生成し、そのメソッドを使うことすらできます。これは、Main.cpp の中で A.h と B.h をインクルードすると、コンパイラが CClassA と CClassB の定義をすべて読み込んで理解した後で、テンプレート引数(=上記コードでは int)に対応するクラス定義を必要に応じて改めて生成するからで、そういう意味でテンプレートクラスはヘッダファイルと相性が良いとも言えるでしょう。と言うか、テンプレートクラスの定義は、ヘッダの中にしか書けないので、コンパイラをそのように作らざるをえなかったと言った方が良いかもしれません。
さて、雰囲気は掴んでもらえたと思うので、ほんの少しだけ複雑な場合を見てみましょう。上記では、クラスのメソッドの中で相手クラスのオブジェクトを作成して、使って、すぐに破棄していましたが、今度は、相手クラスのオブジェクトを指すポインタをメンバ変数にしてみます。メンバ変数は、クラスオブジェクトが作成されてから破棄されるまで、ずっとクラス内部に居続けるので、メソッドの中で一時的に使用するのとは、少し事情が異なる事になるわけです
ちなみに、ポインタではなく、相手クラスのオブジェクトそのものをメンバ変数にすることは、ちょっと無理があることがわかるでしょうか。A のメンバとして B を持つと、その B の中にまた A があり、またその A の中に B があり,...となって、メモリを取るクラスであれば、「メモリがナンボあっても足りない」って事になり、クラスオブジェクトを実体化できないわけです。なので、相手を指すポインタをメンバ変数とします。ポインタは、相手のオブジェクトを一つ指すだけなので、このような無限循環は起きません。
ナオ : 今回のテーマは、無限循環なんですね、店長。
店主 : アレッ。そうだったっけ。
ナオ : またとぼけて。そういう風にしか見えないですヨォ。
店主 : そんなことはないさ。今興味があるのはね、単に A の中の B の中の C の中の D の中の E の中の F の中の...
ナオ : ンまぁ、確かに循環はしてないかァ。
さて、まずは、ヘッダファイルを使わずに、一つの *.cpp ファイルに書いてみましょう。次のようなコードになります。
template <class T> class CClassA; // CClassA はクラスであるということを先行宣言する。 template <class T> class CClassB; // CClassB はクラスであるということを先行宣言する。 // CClassA の定義 template <class T> class CClassA { private: CClassB<int>* m_pclassbi; // CClassB へのポインタだということでコンパイル可能になる。 public: ~CClassA() { delete m_pclassbi; } CClassA() : m_pclassbi(0) {} void FunctionA() { if (m_pclassbi) { m_pclassbi->FunctionB(); } // A の中で B を使う。@ } }; // CClassB の定義 template <class T> class CClassB { private: CClassA<int>* m_pclassai; // CClassA へのポインタだということでコンパイル可能になる。 public: ~CClassB() { delete m_pclassai; } CClassB() : m_pclassai(0) {} void FunctionB() { if (m_pclassai) { m_pclassai->FunctionA(); } // B の中で A を使う。 } }; int main() { CClassA<int> a; CClassB<int> b; a.FunctionA(); b.FunctionB(); return 0; }
上記コード冒頭で、CClassA と CClassB をクラスであると先行宣言していますが、これらの行が無いと、CClassA、CClassB が何者か不明と言うことで、コンパイルエラーになってしまいます。
二つのクラスが互いのポインタを内部変数として持って、相手を使おうと言う場合には、このように、二つの識別子 CClassA、CClassB がクラスであると先行宣言する必要があります。
それでも、上記のようにテンプレートクラスの場合、これだけでメソッドの中で相手のメソッドを使うことが出来ています。ソースコードを@で示した行まで読んだ時点では、CClassB の内容についての記述はまだ行われていないわけで、その記述されていない CClassB のメソッドである FunctionB() を使ってしまっていてもコンパイルは通るというわけです。テンプレートクラスというのは便利ですね。
さて、ここではヘッダファイルがテーマなので、上記コードを CClassA と CClassB を定義する二つのヘッダファイルと、それを使う *.cpp ファイルに分けてみると次のようになります。
<A.h> の中 #pragma onece template <class T> class CClassB; // CClassB の先行宣言 template <class T> class CClassA { // CClassA の定義 private: CClassB<int>* m_pclassbi; public: ~CClassA() { delete m_pclassbi; } CClassA() : m_pclassbi(new CClassB<int>()) {} void FunctionA() { m_pclassbi->FunctionB(); // A の中で B を使う。 } }; <B.h> の中 #pragma onece template <class T> class CClassA; // CClassA の先行宣言 template <class T> class CClassB { // CClassB の定義 private: CClassA<int>* m_pclassai; public: ~CClassB() { delete m_pclassai; } CClassB() : m_pclassai(new CClassA<int>()) {} void FunctionB() { m_pclassai->FunctionA(); // B の中で A を使う。 } }; <Main.cpp> の中 #include "HeaderA.h" #include "HeaderB.h" int main() { CClassA<int> a; CClassB<int> b; a.FunctionA(); b.FunctionB(); return 0; }
上記コードに特に変わった様子は無いのですが、A.h の中では相手の CClassB を先行宣言し、B.h の中でも相手の CClassA を先行宣言していることに注意してください。このように、テンプレートクラスを記述するヘッダファイルの場合には、相手クラスのポインタをメンバ変数とする場合には、相手クラスをクラスであると先行宣言すればよい、ということになります。それでも、相手のヘッダを互いにインクルードはしていませんね。
上記のようにヘッダファイルを作成しておけば、A.h と B.h をインクルードする順番は任意でよいということにも注意してください。
ヘッダファイルトップへ 前のページへ 次のページへ(準備中 ^o^)