VCで初めてC言語に触れた人にとって、「ソースファイルとヘッダーファイル」という概念はかなーり難しいものだと思います。特に「インクルードしてるのになんで”定義されていない識別子です。”なんて言われるのーっ!?」と苦しんでいる方もいるのではないでしょうか。
そこで、今回はこの2ファイルの関係について見ていこうと思います。 |
ソースファイル:コンパイルされるファイル |
まず「ソースファイル」について見てみましょう。
コンパイラはソースファイルをコンパイルします。コンパイルを行うプログラムCL.exeは、引数としてソースファイルひとつを取り、そのファイルをコンパイルします。つまり「ヘッダーファイル」はコンパイルしません。これが重要。ただし、「インライン関数」と「テンプレート関数」については別。これはあとで解説します。 また、ソースファイルはひとつずつコンパイルされます。複数のソースファイルがくっつけられてまとめてコンパイルされることはなく、ソースファイルひとつひとつに対してCL.exeを実行します。コンパイラはソースファイルひとつひとつをコンパイルして、それぞれをオブジェクトファイルに生成します。生成されたオブジェクトファイルはリンカが、ひとつの実行ファイルやDLLへと作り上げます。 このふたつが非常に大事です。よく憶えておきましょう。 コンパイラはソースファイルをコンパイルし、オブジェクトファイルへと変換します。オブジェクトファイルの中には「変数領域の作成」や「数値演算」、「関数の呼び出し」といった意味を持つ機械語が書き込まれます。これは逆に言えば、こういった機能を持つものはソースファイルの中にある必要があるということです。 たとえば、変数の宣言はヘッダーファイルの中に書くことはできません。ソースファイルの中で行うことで、初めて「メモリ領域を確保する」という機械語に翻訳されるのです。クラスの中のメンバ変数も、そのクラス型の変数を宣言したとき、初めてそのメンバ変数も宣言されます。また、クラスのスタティック変数も、宣言はソースファイルの中で行います。 同様に、関数もソースファイルの中で書く必要があります。前述した特別な場合を除いて、ソースファイルの中に関数を書き込むことで、翻訳された機械語が生成されることになります。 では、ヘッダーファイルはどのような役目を負っているのでしょうか。 |
ヘッダーファイル:情報提供 |
ヘッダーファイルには「情報を提供する」」という役目があります。たとえば、次のようなソースファイルがあったとします。
|
void Test0() { Test1( 3 ); //な、なんじゃこりゃぁ!! } void Test1( int p_i1 ) //(そのときこの関数は見えてない) { TRACE( "%d\n", p_i1 ); }
このままではコンパイルエラーが発生します。コンパイラがTest0()を翻訳しようとしたとき、Test1()なる見たことのない存在が関数として呼び出されているからです。
このコンパイルエラーを解決する方法のひとつは、関数の順序を入れ替えることです。たとえば、次のように。 |
void Test1( int p_i1 ) //オレのことを憶えておけよっ!! { TRACE( "%d\n", p_i1 ); } void Test0() { Test1( 3 ); //あ、さっきあったのだ。 }
こうすることで、コンパイルエラーを避けることができます。先にコンパイラに「Test1()ってものが存在するんだぞー」と教えておくことで、あとで「Test1()」というものに出遭ってもうろたえることなくコンパイルすることができるというわけです。この名残で、昔のC言語形式のプログラムはmain()が一番最後にあったりします。
この問題を解決するもうひとつの方法が「プロトタイプ宣言」と呼ばれるものです。 |
void Test1( int p_i1 ); //こういう関数が存在するんだ、憶えとけ!! // (ってのがプロトタイプ宣言) void Test0() { Test1( 3 ); //あ、さっきあったな。 } void Test1( int p_i1 ) //お、これがさっきのの本体だな。 { TRACE( "%d\n", p_i1 ); }
このように、関数の中身を書かず、関数の名前、戻り値の型、引数だけを「宣言」したものを「プロトタイプ宣言」と言います(と言っても、今ではほとんど死語ですが(汗))。このように宣言しておくと、コンパイラは「あ、この関数はどっかにあるものなんだな」と認識して、憶えておいてくれます。で、あとでこの関数が呼ばれたときに、「あ、これがさっき宣言されていた関数だ」と、ちゃんと認識してくれるわけです。
このように、C言語では「ただの情報」と「中身」とを区別することができます。「コンパイラがソースファイル全体を認識してくれない」代わりに、こういった機能が付いていると考えた方がいいかもしれません。 また、この「ただの情報」は「情報提供」しかしません。そして、コンパイラはこの情報を鵜呑みにしてしまいます。 |
void Test1( int p_i1 ); //この関数はあるのです、あるったらあるの! void Test0() { Test1( 3 ); //さっきの関数だ。 } // (Test()1の定義はありません)
このコードはコンパイルエラーが発生しません。コンパイラはTest1()の宣言を見たことで「あ、この関数はどこかにあるんだな」と判断します。そこで、コンパイラはTest1()を呼び出す機械語を書き込みます。よって、コンパイラは通ることになります。
さて、今度はリンカが困ります。Test1()を呼び出した部分が、ちゃんとTest1()の実体部分を呼び出せるようにしなければなりません。でも、そんなものは存在しないので、リンカがエラーを発生させます。 このように、C言語の「ただの情報」は、コンパイラを安心させるために存在しているのです。 で、この「ただの情報」を別のファイルにまとめてしまおう、ということで存在するのがヘッダーファイルです。 |
ヘッダーファイルのインクルード |
MFCを使うアプリケーションともなると、ソースファイルの中に未知の単語が無数に存在します。この単語すべての情報を提供するために、ヘッダーファイルをインクルードする必要があります。
ヘッダーファイルのインクルードは#includeを使用します。このあとに続くファイル名を"と"で囲むとプロジェクトからの相対パス、<と>で囲むと「ツール」−「オプション」−「ディレクトリ」−「インクルードファイル」で設定したフォルダ内を検索します。 インクルードファイルの中に#includeがあれば、さらにそのファイルをインクルードします。たとえば、次のようなヘッダーファイルがあったとします。 |
// これはヘッダーファイルの中です。 #include "KTest.h" // この中にKTestという単語についての情報が入ってます。 void Test0( KTest& p_rkTest ); // 関数の宣言です。 // KTest の情報は、 Test.h の中にありました。
このとき、コンパイラは「より上の行にある」#includeを見つけ、そのヘッダーファイルを先に読み込みます。その中でKTestがなんなのかという情報を仕入れ、そのあとこのヘッダーファイルを読み込んでいき、Test0の宣言でKTestなる存在が現れても戸惑わないというわけです。
このヘッダーファイルの読み込みは、「一直線上」である必要はありません。ヘッダーファイルを読み込んだら、それをコンパイラがちゃんと憶えておいてくれます。たとえば、まず次のTypeDefine.hというヘッダーファイルがあったとします。 |
// TypeDefine.h typedef int & Integer; // int の参照として Integer を定義します。 // よいこのみんなはマネしちゃダメだぞ!
さらに、次のような関数を宣言したFunc.hというヘッダーファイルがあったとします。
|
// Func.h void Test0( Integer p_ri ); //Integer を使ってます。
さて、この関数を使うソースファイルは、次のようになります。
|
#include "TypeDefine.h" //Integerを知り、 #include "Func.h" //Test0()を知る。 void Test1() { int i1 = 3; Test0( i1 ); TRACE( "%d\n", i1 ); }
重要なのは、Func.hはTypeDefine.hを直接的に読み込んでいないことです。
コンパイラはまずTypeDefine.hを読み込み、その中でIntegerの存在を憶えます。次にFunc.hを読み込み、その中でIntegerに出会い、これを先ほど憶えた単語と同一物として処理します。 このように、コンパイラは読み込んだヘッダーファイルの内容を憶えていき、適用していきます。ただし、各ファイルで「上から読み込む」ということには変わりありません。たとえば、上の例でFunc.hを先に、TypeDefine.hをあとに読み込んだらコンパイルエラーが発生します。 |
宣言は一度だけ |
たとえば、次のようなヘッダーファイルはコンパイルエラーになります。
|
// クラスの宣言。 class KTest1 { public: int m_i; }; // あれま、同じクラスを宣言しちゃいましたよ。 class KTest1 { public: int m_i; };
このように、2度同じ宣言をするとコンパイルエラーが発生する場合があります。こういったことはコピーのミスでもないかぎりなさそうですが、実際には似たようなケースが起こりやすかったりします。上の宣言を修正したKTest1.hというファイルがあり、それを使う次のようなソースファイルがあったとします。
|
#include &guot;KTest1.h" //中にはKTest1の宣言(ひとつだけ)が入ってます。 void Test0( KTest1 &p_rkTest1 ) { p_rkTest1.m_i = 3; }
これはちゃんとコンパイラに通ります。ところが、次のようにすると、エラーが発生します。
|
#include &guot;KTest1.h" //1度目。 #include &guot;KTest1.h" //2度目、これでコンパイルエラー。 void Test0( KTest1 &p_rkTest1 ) { p_rkTest1.m_i = 3; }
このように2度インクルードすることで、先ほどの「ふたつKTestが存在する」エラーが発生します。つまり、コンパイラは「同じファイルを読み込んだ」ことを判断できないのです。そのため、同じヘッダーファイルを2度読み込むことで、2度同じクラスが宣言されたと捉えてしまいます。
さらに、コンパイラは「ヘッダーファイルを箇条書き的に読み込む」ため、「気付かないあいだに2度読み込んでいる」ということがたびたび発生します。 この問題を解決する方法のひとつは、#defineを使用することです。たとえば、次のヘッダーファイルのようにします。 |
// KTest1.h ヘッダーファイルです。 // 何度このファイルを読み込んでも1回だけ宣言するためのマクロです。 #if !defined(KTEST1_H) #define KTEST1_H // クラスの宣言。 class KTest1 { public: int m_i; }; #endif //KTEST1_H // このように、必ず #endif で閉じておきましょう。
こうすると、このヘッダーファイルは何度読み込んでも大丈夫になります。
このファイルを最初に読み込んだとき、まだKTEST1_Hは#defineで定義されていないので!defined(KTEST1_H)はゼロ以外を返し、#if以下#endifまでの行が読み込まれます。このとき、初めてKTEST1_Hは#defineされます。 次にこのファイルを読み込んだとき、すでにKTEST1_Hは#defineされているので!defined(KTEST1_H)はゼロを返し、#ifから#endifまではプロプロセッサが削除します。 結果、KTest1はたった一度だけ宣言されるというわけです。 |
ヘッダーファイルはソースファイル毎に |
最初に「大事なこと」として上げた中の2番目に「ソースファイルはひとつずつコンパイルされる」というものがありました。これは、ヘッダーファイルの読み込みに関しても言えることです。
どういう事かというと、ヘッダーファイルは「コンパイルするソースファイルごとに読み込む」ということです。つまりコンパイルするソースファイルが変われば、今まで憶えた「ただの情報」を忘れ去り、ヘッダーファイルをまた新たに読み込み直すのです。 たとえば、次のソースファイルをコンパイルしたとします。 |
// Test1.cpp ソースファイル。 #include "Test0.h" // この中に Test0() という関数の宣言が入ってるとします。 void Test1() { Test0(); //Test0.h の中のTest0()を呼び出します。 }
このコンパイルは通ります。さて、続けてコンパイラが次のソースコードをコンパイルするとします。
|
// Test2.cpp ソースファイル。 // 何もインクルードしていない……。 void Test2() { Test0(); //なんじゃこりゃぁ!! }
このソースコードはコンパイルエラーが発生します。コンパイラがこのソースファイルをコンパイルしようとする直前に、それまで憶えていたヘッダーファイルの情報をすべて忘れ去るからです。そのため、インクルードしていないTest0()の情報がないため、エラーを発生させるわけです。
こうやって分かりやすく見てみると「んなバカなことするわけないやん」とお思いでしょう。でも最初に言った「定義されていない識別子です。」はこれによるものが多いのです。 |
ありがちなパターン |
MFC AppWizard (exe)として、MFCダイアログアプリケーションを作成したとします。次に、ダイアログクラスのメンバ変数として次のようなものを入れたとします。
|
// MyTestDlg.h ヘッダーファイル。 class CMyTestDlg : public CDialog { // 略。 // インプリメンテーション protected: // double型のリスト。 CList<double, double &> m_cDoubleList; };
このCListの宣言はafxtempl.hの中にあります。このヘッダーファイルは、デフォルトではインクルードされません。さて、問題はこのファイルをどこでインクルードするかです。
まず考えられるのが、このソースファイルの方でインクルードするというものです。たとえば、次のように。 |
// MyTestDlg.cpp ソースファイル。 #include "stdafx.h" #include <afxtempl.h> //ここに挿入。 #include "MyTestDlg.h" //この中で CList を使っています。
AfxTempl.hはMFCのヘッダーファイルなので、<と>で囲みます。
注意して欲しいのは、インクルードする位置です。CListはMyTestDlg.hの中で使用されています。つまり、このヘッダーファイルをコンパイラが読み込む前に、CListの情報をコンパイラに教えなければならないのです。 そのため、AfxTempl.hはMyTestDlg.hよりも先にインクルードする必要があるというわけです。 さて、実はこれだけだとコンパイルエラーが発生します。これが、とてもありがちで、結構分かりにくいエラーだったりします。 AppWizardを使用した場合、CWinApp派生クラスのソースファイルから、他のすべてのヘッダーファイルがインクルードされています。 コンパイルはソースファイル毎に行われ、それまで憶えていた情報はすべて忘れるということを説明しました。この条件を踏まえてCWinApp派生クラスのソースファイルをコンパイルした時を考えてみましょう。このソースファイルの先頭では当然ダイアログクラスのヘッダーファイルをインクルードしています。ところがこの段階ではAfxTempl.hをインクルードしていないので、CListについての情報を持っていません。そのため、コンパイルエラーが発生してしまうのです。 この問題を解決するにはどうすればいいのでしょうか。そう、このソースファイルでもインクルードすればいいのです。 |
// Test.cpp ソースファイル。 #include "stdafx.h" #include "Test.h" #include <afxtempl.h> //ここに挿入。 #include "MyTestDlg.h" //この中で CList を使っています。
このように、Test.cppでのヘッダーファイルの読み込みをちゃんと考える事で、コンパイルエラーをなくすことができます。
|
プリコンパイル済みヘッダーファイル |
さて、前項の「ありがちなパターン」で、「なんやね、StdAfx.hでインクルードすればいいやん」とお思いの方も多いでしょう。
もちろんその通りです。StdAfx.hはすべてのソースファイルからインクルードされているので、このファイルでインクルードすれば万事オッケーです。 ですが、MFCを使用せずに自分でクラスライブラリを構築したりする場合には、こういう仕組みを理解してることが大事です。また、このStdAfx.hでインクルードすると面倒なことになる場合もあります。 デフォルトの「設定」では、StdAfx.hは「プリコンパイル済みヘッダーファイル」として指定されています。 今まで見てきたように、ヘッダーファイルのインクルードはそれぞれ連結しているので、とても膨大な量になってしまいます。それをソースファイル毎に読み込むと、かなりのオーバーヘッドになってしまいます。 そこで特定の範囲のヘッダーファイルを、拡張子がpchのバイナリファイルとしてひとまとめにしてしまい、それをコンパイル時に使用することでコンパイルのスピードアップを行っています。このバイナリファイルが「プリコンパイル済みヘッダーファイル」と呼ばれるものなのです。 StdAfx.hはデフォルトでこの設定になっているため、このファイルを読み込み始めてからあとにインクルードしたすべてのヘッダーファイルをひとまとめにします。MFCのヘッダーファイルのように、もう変更することのないファイルを使用する場合にはとても便利な機能です。 が、自作ライブラリなど、頻繁に変更するようなヘッダーファイルを読み込むと大変なことになります。このpchファイルの作成にはかなりの時間が掛かるため、コンパイル時間が長くなってしまいます。 StdAfx.hのなかでインクルードするのは便利ですが、時と場合によるでしょう。もしその「時と場合」に合わないヘッダーファイルを使用するときのためにも、ヘッダーファイルの仕組みをよく憶えておきましょう。 |
C++の機能 |
C++の機能の中にはヘッダーファイルのインクルードに関わってくるものがいくつかあります。
「テンプレート」は、クラスや関数内の特定の単語をコンパイル時に任意に変更できる機能です。このテンプレートを使った関数は、関数そのものの実装もヘッダーファイルの中で行う必要があります。たとえば、次のように。 |
// TmplTest.h ヘッダーファイル。 template< class T > class CTemplate { public: void Set( T p_i ); T Get() const; private: T m_Data; }; template< class T > void CTemplate<T>::Set( T p_i ) { m_Data = p_i; } template< class T> T CTemplate<T>::Get() const { return m_Data; }
このように、テンプレートの場合にはメンバ関数の定義もヘッダーファイルに作成してしまいます。ところが、実際にはこれは何の不思議もありません。なぜなら、このコードはコンパイルされないからです。
テンプレートはクラスや関数の「鋳型」です。これそのものはコードとはみなされません。あとで文字が置き換えられることが前提になっているため、コードが不完全だからです。完全なコードとみなされるのは、これらのクラス型の変数が作成されたときです。 |
// Test.cpp ソースファイル。 #include "TmplTest.h" void TmplTest() { CTemplatecInt; cInt.Set( 3 ); }
このように、テンプレートクラスで置き換える文字を指定し、その型の変数を作成したとき、初めてクラスの実体が作成されます。つまり、感覚的には「テンプレートクラスを使うソースファイル(ここではTest.cpp)の一番最後に、文字の置き換えられたテンプレートクラス(つまりちゃんとしたクラス)が作られ、メンバ関数の定義が行われる」と考えれば分かりやすいでしょう。使用したソースファイルにテンプレートクラスを書き換えた実装を持つクラスが挿入されるというわけです。
似たような仕組みに、「インライン関数」というものがあります。 |
// InlineTest.h ヘッダーファイル。 class CInline { public: void Test1() //これがインライン関数。 { TRACE0( "In CInline::Test1()\n" ); } void Test2(); }; inline void CInline::Test2() //これもインライン関数。 { TRACE0( "In CInline::Test2()\n" ); }
クラス内で定義された関数や、ヘッダーファイル内にinlineを付けて定義された関数は「インライン関数」と呼ばれます。
インライン関数は、関数を呼び出したコードの中に直接埋め込まれます。たとえば、 |
// Test.cpp ソースファイル。 #include "InlineTest.h" void InlineTest() { CInline cInl; cInl.Test1(); cInl.Test2(); }
は、
|
// Test.cpp ソースファイル。 #include "InlineTest.h" void InlineTest() { CInline cInl; TRACE0( "In CInline::Test1()\n" ); TRACE0( "In CInline::Test2()\n" ); }
のように置き換えられます。ただし、場合によっては置き換えられません。その辺はコンパイラや設定によって違うので、各自調べてみてください。どちらにしろ、テンプレートと同じように、ヘッダーファイル内の関数がソースファイルに埋め込まれるわけです。
C++はC言語と違って、再利用やタイプセーフ(型チェックがちゃんとされているということ)のためのシステムが整っています。テンプレートやインライン関数はそういったシステムに含まれるものです。これらは基本的には#defineの代わりなので、ソースファイルへと埋め込まれる形になります。そして、その文法が、分かりやすい関数形式になったと考えればいいでしょう。 |
まとめ |
今回の問題は、どちらかというと「MFCの弊害」と言えるでしょう。普通のC言語プログラミングを経験せずにいきなりMFCを使い始めると、こういった部分が分かりにくくなってしまうからです。
でも、個人的には「最初にMFCを使い始めて、そこから掘り下げていく」というアプローチは決して悪くないと思います。ま、そのためのこのページとでも思ってください(笑)。 |
(C)KAB-studio 1998 ALL RIGHTS RESERVED. |