この記事の対象者
C言語で複数ファイルに分けて書きたい人。C言語のコンパイルの仕組みを知りたい人。ヘッダファイルに書くものがわからない人。
概要
webで調べていると、意外とコンパイルの仕組みやファイル分割について体系的に説明している記事が少なかったので、書いてみることにしました。
ファイルを分割してビルドするとき、どのような仕組みになっているのか、ヘッダファイルやソースファイルには何を書くのがいいのかなどについて説明していきます。
コンパイルとは
人間が理解できるコンピュータ言語をCPUがわかる言葉に翻訳する作業のことをいいます。ちなみに、似た言葉でビルドがありますが、コンパイルはビルドのプロセスの一部です。
必要なツール
- コンパイル環境
ツールチェイン、プリプロセッサ、コンパイラ、アセンブラ、リンカ - 自作のプログラムソース
自分で書いた「.C」などのソースファイル - ライブラリ群
実行ファイル作成に必要な構成要素
普段Visual StudioなどのIDEを使っていると意識しませんが、実行ファイルを作るためには以下の要素が必要です。
- ソースファイル
- ライブラリ部品
- ヘッダファイル
- ライブラリファイル
- 設定ファイル
- その他
アイコン、画像、音声など
プロセス実行までの流れ
ビルドからプロセス実行までの流れは以下の図のようになっています。
プリプロセス
コンパイル前の仕事であり、頭に「#」が付いている文の処理です。例えば以下の文があります。
-
#include
インクルード先のファイルに対して、指定されたファイルの内容を差し込む命令です。ヘッダファイルはこの命令が書かれたソースの位置に差し込まれます。つまり、インクルードした先のソースファイルに、ヘッダファイルの中身が丸々書いてあるイメージです。 ヘッダファイルの中身をコピペしてると言い換えてもいいかもしれません。 これは後のextern宣言ですごく重要なので覚えておいてください。
また、必要のないファイルでインクルードするとビルド時間の増加につながるため、できるだけ避けるようにしましょう。
-
#define
定義した文字列を別の文字列に置き換える命令です。これによって、数字を文字に置き換えたり、関数を文字列に置き換えたりできます。 -
#if,#endif
この命令で指定した文字列が存在したら#if~#endまでをファイルに展開します。 -
#pragma
コンパイラ、リンカへの命令です。環境によって命令が違います。
コンパイル
コースコードをアセンブリ言語へ変換する作業です。この時点ではアセンブリ命令で書かれたファイルが作成されます。
アセンブル
コンパイルによって生成されたアセンブリ言語を機械語に翻訳する作業です。この作業を行うことでオブジェクトファイルが作成されます。オブジェクトファイルにはバイナリでコンピュータへの命令が書かれています。
リンク
必要なライブラリ部品をmain()と繋げる作業をリンクといいます。オブジェクトファイル作成後に行われます。ライブラリとは、例えばprintf(),exit(),myfunc()などmain()がないファイルに書かれた関数群のことをいいます。
main()のあるソースファイルをコンパイルした時点では、printf()等は関数のヘッダで定義されているだけで、中身はないです。リンクを行うことで中身(実際の命令)がわかるようになります。これを名前解決と呼びます。
スタティックリンク
.lib(.a)ファイルなどの静的ライブラリを実行ファイルとリンクすることをスタティックリンクといいます。すべてのソースコードが実行ファイルにまとまるため、取り回しが楽になります。
デメリットとして実行ファイルのサイズが大きくなってしまいます。そこで多くのアプリケーションでは次に説明するダイナミックリンクを使用しています。
ダイナミックリンク
実行ファイルを実行時に動的ライブラリをリンクすることをダイナミックリンクといいます。
パソコンのアプリをインストールすると、インストールしたフォルダに.dll(.so)の拡張子を持ったファイルが入っていると思いますが、それが動的ライブラリといいます。動的ライブラリはパスを使ってリンクするため、正しく配置されている必要があります。
ファイルについての説明
それぞれのファイルに書くべきことについて書いていきます。
ヘッダファイル(.h .hpp)
書いていいこと
- プリプロセスで定数の定義
- 関数の定義
- 構造体やクラスの定義
- extern宣言
普通は書かないこと
- 変数の定義(例:
int a = 0
) - 関数、メソッドの中身の記述
ソースファイル(.c .cpp)
- 関数の中身の記述
- グローバル変数の定義
- 変数の定義
- 関数、構造体、クラスなどの定義
分割コンパイルについて
複数ファイルに分けてコンパルする仕組み(分割コンパイル)について説明します。
分割コンパイルの仕組み
分割コンパイルの仕組みは以下の図のようになっています。基本的なプロセスはソースファイルが1つのときと一緒で、オブジェクトファイルの作成までをそれぞれのファイルごとに行い、できたオブジェクトファイルを結合して一個の実行ファイルを作成します。結合するときにリンクを行います。
分割コンパイルのメリット
- 効率的なビルドができる
複数のファイルを同時並行でコンパイルするなど、効率的なビルドが可能となる。 - モジュール化と保守性
関連のある処理毎にファイルを分けることで1ファイルが短くなるため、可読性が上がる。また、管理がしやすくなる。 - コンパイル時間の削減
変更のあったファイルだけをコンパイルすればいいため、ソースコードの規模が大きい場合にコンパイル時間を削減することができる。
extern宣言
extern宣言とは?
あるグローバル変数を他のソースファイルの中のどこかで宣言してあるので、後でリンク時に解決せよという宣言。
extern宣言の使い所
ヘッダファイルの中で宣言することが多いです。extern宣言したヘッダファイルをincludeすれば、ファイル中に実体がなくても、そのグローバル変数を使うことができるようになります。
ヘッダファイルに変数を定義するとどうなる?
いろいろなファイルで1つのグローバル変数を使いたい場合、ヘッダファイルに書くことが一般的かと思います。その時に、extern宣言を使わずにヘッダファイルで定義すると、ある変数の実体がインクルード先のファイルに別々にできてしまい、多重定義でビルドエラーになります。
例えば、以下のソースコードをビルドすると、Hoge
が多重定義になってしまい、エラーになります。正しく動かすにはどうすればいいのでしょうか?
# pragma once
# define MY_DATA (10)
int MyFunc(int a, int b);
int Hoge = 10; //グローバル変数
# include <stdio.h>
# include "func.h"
int main(void)
{
int result = 0;
Hoge = 100;
result = MyFunc(3, 2);
printf("result=%d\n", result);
}
# include "func.h"//ここにヘッダファイルが展開されるので、実体がmain.cと2つできてしまい、二重定義になってしまう。
int MyFunc(int a, int b)
{
return a + b + Hoge;
}
extern宣言を使う
extern宣言とは、"どこかのファイルにextern宣言した変数の実体があるので後でリンクしてください"という宣言です。これにより、分割したソースファイルでHoge
の定義を書いていなくても、ヘッダファイルでextern宣言をしていれば、Hoge
の定義を探してリンクしてくれます。
それでは、先程のfunc.hでHoge
に対してextern宣言をし、main.cにHoge=10
と定義してみましょう。こうするとfunc.hのHoge
はmain.cのHoge
とリンクされて実体が一つになるため、ビルドエラーになりません。
# define MY_DATA (10)
int MyFunc(int a, int b);
extern int Hoge; //グローバル変数のextern宣言
# include <stdio.h>
# include "func.h"
int Hoge = 10;
int main(void)
{
int result = 0;
Hoge = 100;
result = MyFunc(3, 2);
printf("result=%d\n", result);
}
# include "func.h"
int MyFunc(int a, int b)
{
return a + b + Hoge;
}
まとめ
今回は、C言語の分割コンパイルについてextern宣言も絡めて説明しました。この記事が参考になれば嬉しいです。