LoginSignup
175

More than 5 years have passed since last update.

【C++】C++のヘッダインクルード周りの話 その1(includeの目的と分割コンパイルの基礎)

Last updated at Posted at 2014-11-20

概要

C++のヘッダはインクルードが必要なのか不要なのか判らなかったリ、
順序によってどのような問題が起きるのか判らんけどとりあえず#pragma onceつけとけば問題無いよねとか、
大きな開発の場合初めからちゃんとやっておかないと後々解決しようがない程の問題(コンパイル速度)に発展しかねない危険も孕んでいたりと手軽に利用できる割には闇が深いです。

そこらへんについて自分の知識でまとめてみようと思うので、
間違っていたりこんな考えもあるみたいなのもあったら是非コメントでまさかりを投げてください!

出来る限り初心者の方でも判るように説明する予定です。よく判らないとかあったら言ってくれれば頑張ります。

結構長い解説になりそうなので数回に分けようと思います。

今回は初回なのでとりあえずインクルードの基礎的な知識について解説します。

includeする目的を考える

関数やクラスの宣言はヘッダに書き中身はcppに書いてヘッダをインクルードするというのが暗黙のルールと化していますが別にそうでなくても構いません。
inlcudeは指定されたファイルをその位置(行数目)に単純に展開するだけの機能なので、

// XMLを読み込む
return ImportXML(
#include "test.xml"
);

みたいなのも開発効率が上がるなら特に問題ないと思ってます。
昔DLLの関数リストを作るのにこの方法を用いたことがあります、結構便利でしたよ。
ようは目的がしっかりしてれば用途は何でも良いわけです。

一般的な用法である宣言をヘッダに書いてインクルードしてcppに定義を書くというのもいくつかの目的があります。

  1. typedefやマクロをヘッダに書いて共有する
  2. 別のcppや静的ライブラリに定義された関数やクラスを利用する

よくある勘違いは関数やクラスを使いまわす為にインクルードを利用するというものです。
意味的には2と同じじゃんって感じですが、ようはヘッダーファイルに関数の定義も全部書くって話ですね。inline関数とかいうやつです。

aaa.cpp
inline static int aaa() // inline関数.
{
    return 100;
}

当然有効な場合もありますがちょっと特殊なのでこれについては別の機会に解説をまわします。

どちらかと言えばヘッダを利用する目的の殆どは別のcppに定義された関数を別のcppから呼び出すという点が重要かと思います。

今回の記事ではこれに関して解説します。

別のcppに定義された関数を呼び出すのにインクルードは必須ではない

例えば次のようなプログラムがあるとします。

FuncA.cpp
int FuncA()
{
    return 100;
}
main.cpp
int main()
{
    int a = FuncA(); // コンパイルエラー.
    return 0;
}

main.cppからFuncAは見えないので呼び出すことが出来ません。
よってここでコンパイルエラーとなります。
エラーの内容は「FuncAってなんだよ」と言うエラーです。コンパイラはFuncAなんて単語は知らないのでコンパイルできないわけです。

よってこの問題を解決する方法は3つあります。

  1. main.cppでFuncA.cppをインクルードする
  2. main.cppの頭にFuncAの宣言を書く
  3. FuncA.hにFuncAの宣言を書いてそれをインクルードする

main.cppでFuncA.cppをインクルードする

別にヘッダでなくてもインクルードできることは先述したとおりです。
main.cppでFuncA.cppをインクルードすればヘッダなんていらないわけですね。

FuncA.cpp
int FuncA()
{
    return 100;
}
main.cpp
#include "FuncA.cpp" // FuncA.cppをインクルードしてしまう.
int main()
{
    int a = FuncA(); // これで利用できる!.
    return 0;
}

しかし実際コンパイルすると殆どの方は次のエラーが出てコンパイルに失敗したはずです。

main.obj : error LNK2005: "int __cdecl FuncA(void)" (?FuncA@@ YAHXZ) は既に FuncA.obj で定義されています。
fatal error LNK1169: 1 つ以上の複数回定義されているシンボルが見つかりました。

どういう意味でしょうか?

先ほどのプログラムのインクルードを展開してみればわかるはずです。

FuncA.cpp
int FuncA()
{
    return 100;
}
main.cpp
int FuncA() // ←インクルードを展開してFuncAがmain.cppにも定義された.
{
    return 100;
}

int main()
{
    int a = FuncA(); // コンパイルエラー.
    return 0;
}

このFuncA.cppとmain.cppにはFuncA関数の定義が2つ存在します。
これをコンパイルして生成される実行ファイル(拡張子が.exeのファイルのことです)は次のようになってます。
(本当は実行ファイルは数値列なんですが説明にならないので敢えて文字で書きます)

実行ファイル.exe
int FuncA()
{
    return 100;
}

int FuncA()
{
    return 100;
}

int main()
{
    int a = FuncA();
    return 0;
}

非常に雑多な解説で申し訳ないんですが実行ファイルは各種ソースを1つにまとめたものと考えるのが判りやすいと思うのでこうしました。

これを見る限りでは「実行ファイル.exe」の中に「FuncA」関数が2つ入ってますね。
重複する関数が2つあるとどっちを呼び出せばいいのか判らない等の問題もあるのでエラーになるわけです。

「ってことはcppインクルードするのだめじゃん!」

って言われそうですがFuncA.cppをコンパイルしなければ済む話です。

多分C++書いてる人の多くはVisual Studioだと思うのでそっちで話を進めますが、
プロジェクトにcppファイルを追加するとそれはコンパイルの対象になります。

例えばプロジェクトにmain.cpp, FuncA.cppの2つを追加したら両方コンパイル対象になります。

funca_main.png

下記のようにmain.cppだけならFuncA.cppはコンパイルされません。

main_only.png

main.cppはFuncA.cppをインクルードしていますが、例えインクルードされていようとコンパイルされません。
この場合、

main.cpp
#include "FuncA.cpp"
int main()
{
    int a = FuncA();
    return 0;
}

というプログラムが先ずインクルードが展開されて

main.cpp
int FuncA()
{
    return 100;
}

int main()
{
    int a = FuncA();
    return 0;
}

となり、後はこれがそのままコンパイルされるので生成された実行ファイル(*.exe)は

実行ファイル.exe
int FuncA()
{
    return 100;
}

int main()
{
    int a = FuncA();
    return 0;
}

となって関数名は重複せず正しくコンパイルできます。

ただしこの方法はmain.cpp以外に別のaaa.cppとかがあってそこでもFuncA関数を使いたいと思うと破綻します。
main.cppとaaa.cppでFuncA.cppをインクルードするとさっきみたいに関数が重複するからです。

main.cpp
#include "FuncA.cpp"
int main()
{
    int a = FuncA();
    return 0;
}
aaa.cpp
#include "FuncA.cpp" // 2つ目のインクルード!ここで関数が重複する!
int aaa()
{
    int a = FuncA();
    return 0;
}

さて、気づいた方もいるかもしれませんが、
実はヘッダーファイルに関数の定義を書く場合も全く同様の問題が起こります。
インクルードしているcppが複数あると関数が重複してしまいコンパイルが通らないわけです。

だからヘッダーファイルには関数の宣言だけ書くわけですね。

main.cppの頭にFuncAの宣言を書く

さて、前章の最後に
「だからヘッダーファイルには関数の宣言だけ書くわけですね。」
とか言ったわけですが。

関数の宣言というのは関数の中身を取り除いた名前だけのコードです。
FuncAで言えば下記が関数の宣言にあたります。

// FuncA関数の定義。FuncAから{}で囲まれた部分を全部削除したものが宣言になる
int FuncA();

これをヘッダに書いてmain.cppでインクルードすればいいわけですよね。

FuncA.cpp
int FuncA()
{
    return 100;
}
FuncA.h
int FuncA();
main.cpp
#include "FuncA.h"

int main()
{
    int a = FuncA();
    return 0;
}

これはコンパイルが通らないわけがないのですが。

ところでインクルードは単純にファイルを展開しているだけと言いました。
main.cppのincludeを展開すると次のようになります。

FuncA.cpp
int FuncA()
{
    return 100;
}
main.cpp
int FuncA(); // 関数の宣言をmain.cppの頭に記載.

int main()
{
    int a = FuncA();
    return 0;
}

これはコンパイル通るかと言うとこれも当然通ります。

「別のcppに記述された関数を呼びだすのに必要なのは関数の宣言だけ」

です。関数の宣言が関数を利用する前に書かれていればコンパイルが通るわけです。

何で宣言だけあれば良いかというとコンパイルの話になってしまうのですが、ヘッダのインクルードとコンパイルの話は切っても切れない関係にあるので、
出来る限り判りやすくさっくりと解説してみます。

尚実際のコンパイルの手順について私も全ては把握してません。こんな感じになってるという大雑把な解説となります。正しい知識を得たい場合は別途参考書等で勉強してください

先ずコンパイラというのはC言語の仕様に沿っていない単語が出てくるとエラーを起こすソフトです。
if, else, int, char, ...といったいわゆる「予約語」と呼ばれる単語ですね。
これ以外の文字はコンパイラは理解できないので全てエラーになります。

よってさっきから出ている自作関数のFuncAという関数はエラーの対象です。
しかし、こいつが出てきてもエラーが出ないようにする方法があります。それが宣言です。

cppプログラム中に宣言を記載すると、「そのcpp内のそれ以降の処理でその関数を利用するんだ」という事をコンパイラに伝えることが出来ます。

main.cpp
// FuncAの関数を宣言する。
// コンパイラはこの宣言以降でFuncA関数を呼びだす事を事前に知る事が出来る。
int FuncA();

int main()
{
    // ここで実際に関数が呼び出される。
    // コンパイラは宣言を見たのでFuncAが使われる事を知っているからコンパイルエラーにしない。
    int a = FuncA();
    return 0;
}

しかし上記のプラグラムにはFuncAが結局何をする関数なのか、つまり関数の中身が書いてありません。

関数の中身とは「関数の定義」のことです。
定義はFuncA.cppに書いてありますよね。

FuncA.cpp
int FuncA()
{
    return 100;
}

別のcppに書いてあるのでは見えないしヘッダもインクルードしていないのにどうしてコンパイルが通るのでしょうか?

コンパイラはとりあえず全てのcppファイルをコンパイルした後、
「関数の定義」と「実際に呼び出されている箇所」を組み合わせて実行ファイル(*.exe)を組み立てる段階に入ります。

例えば今回main関数内でFuncAを呼んでいるので、
FuncAが来たらFuncAの場所に飛ぶみたいな処理をコンパイラが生成するんですがこれは言葉だけではちょっと難しいですね・・・

実際に生成される実行ファイルの中身をかなり噛み砕いて表示してみます。
左側の数字は行数です。

実行ファイル.exe
 1 最初に8行目へ飛ぶ
 2
 3 int FuncA()
 4 {
 5      100を返して10行目に飛ぶ // return 100;
 6 }
 7
 8 int main()
 9 {
10      int a = 3行目へ飛ぶ
11      プログラム終了 // return 0;
12 }

こんな感じです。関数呼び出しは「ある特定の行に飛ぶ」と言う処理に置き換わる・・・と、考えてください
(実際は違いますが詳細を語れる程の知識は無いのでそこまで知りたい人は別途参考書を買ってください)。

この「どこからどこに飛ぶ」みたいなのを組み合わせるような事をして、
コンパイラは実際に実行ファイルを生成するわけです。

関数の定義が無いと、この「飛ぶ先」が無くなってしまうのでコンパイルが通りません。

ちなみに定義がない場合に出るエラーは「未解決の外部シンボル」のエラーです。1度は見たことあるのではないでしょうか。昔このエラーにひたすら悩まされました・・・

main.obj : error LNK2019: 未解決の外部シンボル "int __cdecl FuncA(void)" (?FuncA@@ YAHXZ) が関数 _main で参照されました。
fatal error LNK1120: 1 件の未解決の外部参照

さて、色々と話しましたがこんな感じで関数の宣言さえあれば別のcppに書かれている関数は呼び出すことが出来ます。まとめると、

「関数を呼びだすのに必要なのは関数の宣言のみ」

です。これは本当に重要なことなので絶対に忘れないでください!

FuncA.hにFuncAの宣言を書いてそれをインクルードする

長々と解説してやっと誰しもがやっているヘッダに書いてインクルードするという方法の解説に来ました。

ところでFuncAの宣言を書くのは「FuncAを使うのに必要なのが関数の宣言だけだから」です。

ではなぜmain.cppに直で宣言を書かずに敢えてヘッダファイルを用意してそこに宣言を書くのでしょうか?

それは保守性の問題です。
例えばFuncAの関数名や引数が変わったとき、
そこら中に散乱した全ての宣言を書き換えるのは大変です。

だから宣言を1つのファイルに書いておいて、それをインクルードするだけにしておけば修正が容易でメンテしやすいよねという話です。

とはいえ、
「関数名や引数が変わると結局呼び出し元も全部修正することになるじゃん」
という突っ込みもあります。

しかしちょっと考えてみてください。
関数の宣言とは「コンパイラにこの関数を利用するよ!」という事を教えているんでしたよね。
つまり定義側の関数名が仮に変わったとしてもコンパイル事態は通ってしまい、未解決のエラーが表示されてしまうわけです。

例えば先ほどのFuncAと言う関数がFuncBになったとします。
先ほどと同様にヘッダに宣言は書かずmain.cppでFuncA関数の宣言を書いておきます。

FuncA.cpp
int FuncB() // 関数名が変わった!
{
    return 100;
}
main.cpp
int FuncA(); // 関数の宣言や使っている箇所は関数名が変わってない!

int main()
{
    int a = FuncA();
    return 0;
}

この状態でコンパイルすると次のエラーが出ます。

main.obj : error LNK2019: 未解決の外部シンボル "int __cdecl FuncA(void)" (?FuncA@@ YAHXZ) が関数 _main で参照されました。
fatal error LNK1120: 1 件の未解決の外部参照

これではどこがエラーなのかさっぱり判らないですよね。
FuncAを使うと宣言し実際に使ってはいるけど、FuncAの定義が無いよというエラーが出てしまっています。

本来コンパイラに出して欲しいエラーは「FuncAという関数はFuncBという関数名に変わったよ」って感じのエラーです。

しかしコンパイラからすれば「FuncAという関数を使いたいのに存在しない」のか「FuncAという関数の名前が単に変わってしまっただけ」なのかは判らないので、関数が存在しないというエラーを出してしまっています。

関数の宣言をヘッダにまとめておかないとこういった都合の悪い事態に陥ってしまいます。

(コラム)hello worldに#include はいらない

関数を呼びだすのに必要なのは関数の宣言だけです。
それはprintf等の標準で用意されてる関数でも当然同様です。

stdio.hをインクルードしないhello worldは次のようになります。

main.cpp
// stdio.hをインクルードする代わりにprintf関数の宣言を頭に書く
extern "C" {
    int printf(const char * _Format, ...);
}

int main()
{
    printf("hello word");
    return 0;
}

extern Cとかいうのが気色悪いですが、ようはprintfの宣言を頭に書いて
「こういう関数使いますよ」
とコンパイラに教えてさえ上げれば、コンパイルできるわけですね。

ところでprintfが記載されてるcppはどこなんでしょうか?
実はこれは静的ライブラリの中です。
cppだけでなく静的ライブラリでも宣言さえあれば呼び出すことが可能です。

extern Cがどうしても気になる人がいるかもしれないのですが、知識が浅く正しい解説ができそうにないので各自調べて頂きたいです。

そんな感じでヘッダのインクルード周りの話は割とコンパイルの話に寄りがちですが出来る限り脱線せずプログラムを書く方向の話で進めていければと思います。

次回はインクルード順序とかインクルードガードあたりについて解説予定です。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
175