対象 | C言語初心者 |
概要 | C言語のファイル分割のやり方について記載しています。 |
筆者 | Zuu [ ] 大手電機メーカーでの製品開発およびマネジメント経験がある現役エンジニアです。 プロジェクトマネージャ、システムアーキテクトなどの資格を保有しています。 |
はじめに
これまで説明してきたプログラムは、「main.c」という1つのファイルに定義や関数などをすべて記述してきました。
大規模なプログラムを開発する場合は、1万行をはるかに超えるようなプログラム規模になるため、1つのファイルにすべて記述するのは現実的ではありません。
また、実際のプログラム開発の現場では、以前作成したプログラムを再利用して使うことがあります。
プログラムを再利用する場合、1つのファイルから必要な部分を探してコピーしてくるというようなことは行わず、あらかじめよく使われる機能をまとめたファイルを作っておき、ファイル単位で再利用を行います。
ファイル単位での再利用を実現するために必要となる概念が「ファイル分割」です。
C言語は、最近のプログラミング言語と比べてファイル分割のやり方が少し難しいため、基本的な仕組みや方法をきちんと理解しておくことが重要です。
C言語でファイル分割を行うには、「ヘッダファイル」を上手く使いこなす必要があります。そのため、まずはヘッダファイルについての説明をしていきます。
C言語のヘッダファイルの作り方
ヘッダファイルとは?
ヘッダファイル(英:Header file)は、特にC言語やC++でのプログラミングで使われるファイルであり、一般にソースコード形式をしていて、コンパイラが別のソースファイルの一部として自動的に展開して使用する。ヘッダファイルには、サブルーチンや変数やその他の識別子の前方宣言が含まれていることが多い。
今まで特に意識せずに使用してきたかもしれませんが、stdio.hやstring.hなどの「include文で指定しているファイル」がヘッダファイルです。
include文は、指定したヘッダファイルの中身をソースファイルの中に取り込む(展開する)命令です。図にすると以下のような感じです。
よって、今までソースファイルに記述していた「サブルーチンや変数やその他の識別子の前方宣言」をヘッダファイルに記述し、その前方宣言を使いたいソースファイルでヘッダファイルをincludeするという形にするのが基本となります。
前方宣言とは、具体的には以下のものを指します。
- 定数の定義
- 構造体の定義
- 関数の定義(プロトタイプ宣言)
- グローバル変数の定義(外部変数)
次の節からは、ヘッダファイルの中身の記述方法について説明していきます。
二重include防止
ヘッダファイルを作成したら、まず最初に記述するのが「二重include防止」です。
ヘッダファイルは、ソースファイルだけでなくヘッダファイルからもincludeすることが可能となっています。そのため、どのヘッダファイルからどのヘッダファイルがincludeされているのかを正確に把握するのが困難であり、同じヘッダファイルをincludeしてしまうことがあります。
二重include防止が記述されていないヘッダファイルを複数回includeすると、同じ定数や同じ構造体を複数回定義する形となり、コンパイル時にエラーとなります。
このような複数回定義を避けるために、必ず二重include防止を記述するようにしましょう。二重include防止は、具体的には以下のように記述します。
1 2 3 4 5 6 |
#ifndef ABC_H /* ABC_Hが定義されていなければ(2回目は通過しない) */ #define ABC_H /* ABC_Hを定義 */ /* 各種定義 */ #endif /* ABC_H */ |
定数の定義
ヘッダファイルに記述すべき前方宣言の1つ目は、「定数の定義」です。定数とは、今まで「#define」を使用して定義したものです。
定数の定義をヘッダファイルに記述するにあたり、注意すべき点があります。
それは、同じ名前の定数が複数回定義されると、コンパイル時にエラーとなることです。よって、「X」のような簡単な名前は重複しやすいため避けるべきです。
独自の定数名となるようにするには、ファイル名を使う方法がよく用いられます。例えば、「abc.h」というファイルに記述する定数であれば、以下のような名前にします。
ABC_X
ABC_Y
また、#define以外に「列挙型(enum)」という機能でも定数を定義することができます。列挙型は、値を自動で決めてくれる(+1してくれる)特性があるので、値をあまり気にしなくても良い場合や、1ずつ変化する定数を複数個定義したい場合などに役立ちます。
値を自分で決めることもできるので、目的に応じて上手く活用しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#ifndef ABC_H #define ABC_H /* #defineを使用した定数定義 */ #define ABC_X (10) #define ABC_Y (20) /* 列挙型(enum)を使用した定数定義 */ enum { ABC_Z1 = 0, /* 先頭要素の値定義を省略した場合は「0」となります */ ABC_Z2, /* 自動で「ABC_Z1 + 1」となり「1」となります */ ABC_Z3 = 5, /* 値を自分で決めることもできます */ ABC_Z4 /* 自動で「ABC_Z3 + 1」となり「6」となります */ }; #endif /* ABC_H */ |
構造体の定義
ヘッダファイルに記述すべき前方宣言の2つ目は、「構造体の定義」です。
構造体って何?って方は、以下の記事で説明しているので、よろしければご覧ください。
定数と同じく、同じ名前だとコンパイルエラーになるので、独自の構造体名となるようにしましょう。名前の付け方は、上述の「定数の定義」を参考にしてください。
1 2 3 4 5 6 7 8 9 10 11 |
#ifndef ABC_H #define ABC_H /* 構造体の定義 */ typedef struct abc_person { char name[16]; /* 名前 */ int height; /* 身長 */ int weight; /* 体重 */ } ABC_PERSON; #endif /* ABC_H */ |
関数の定義(プロトタイプ宣言)
ヘッダファイルに記述すべき前方宣言の3つ目は、「関数の定義」です。
C言語では、関数を呼び出す際には、関数がどのような名前で、どのような型(戻り値、引数)なのかを知っている必要があります。
関数がどのような型なのかをあらかじめ定義する方法として、「プロトタイプ宣言」がありましたね。関数って何? or プロトタイプ宣言って何?って方は、以下の記事で説明しているので、よろしければご覧ください。
よって、別ファイルにある関数を呼び出したい場合には、関数のプロトタイプ宣言をヘッダファイルに記述する必要があります。
1 2 3 4 5 6 7 |
#ifndef DEF_H #define DEF_H /* プロトタイプ宣言 */ int def_func(int a, int b); #endif /* DEF_H */ |
グローバル変数の定義(外部変数)
ヘッダファイルに記述すべき前方宣言の4つ目は、「グローバル変数の定義」です。
C言語では、他のファイルに定義されているグローバル変数を参照することができますが、少し特殊な定義が必要となります。
具体的には、変数宣言の前に「extern」という定義を付与します。
「extern」をつけ忘れると、includeしたファイルすべてで同じ名前のグローバル変数が宣言されてしまい、コンパイルエラーとなるので忘れないようにしましょう。
1 2 3 4 5 6 7 |
#ifndef DEF_H #define DEF_H /* グローバル変数の定義 */ extern int def_result; #endif /* DEF_H */ |
C言語でのファイル分割のやり方
本記事の冒頭で、「あらかじめよく使われる機能をまとめたファイルを作っておきファイル単位で再利用を行う」と 述べました。
具体的には、ソースファイルとヘッダファイルの拡張子以外を同じ名前にすることが多いです。この後に掲載している例では、「def.c」「def.h」としています。
「main.c」などのファイルから、「def.c」に定義されている機能を利用するには「def.h」をincludeします。
※printf関数を使用したいためにstdio.hをincludeするのと同様です。
また、共通のパラメータなど、値を持つだけで具体的な処理がないような定義に関しては、対になるソースファイルがなくてもヘッダファイルにまとめることがあります。この後に掲載している例では、「abc.h」としています。
main.c | abc.h/def.hで定義されたものを利用。 |
abc.h | 共通で使用されるものを定義。 |
def.h | def.cの機能を利用するための前方宣言を記述。 |
def.c | 関数、グローバル変数を定義。 |
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <stdio.h> #include "abc.h" #include "def.h" int main(void) { ABC_PERSON person; int result; printf("ABC_X = %d\n", ABC_X); printf("ABC_Y = %d\n", ABC_Y); printf("ABC_Z1 = %d\n", ABC_Z1); printf("ABC_Z2 = %d\n", ABC_Z2); printf("ABC_Z3 = %d\n", ABC_Z3); printf("ABC_Z4 = %d\n", ABC_Z4); result = def_func(10, 5); printf("result = %d\n", result); printf("def_result = %d\n", def_result); return 0; } |
abc.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#ifndef ABC_H #define ABC_H #define ABC_X (10) #define ABC_Y (20) enum { ABC_Z1 = 0, ABC_Z2, ABC_Z3 = 5, ABC_Z4 }; typedef struct person { char name[16]; int height; int weigth; } ABC_PERSON; #endif /* ABC_H */ |
def.h
1 2 3 4 5 6 7 8 9 |
#ifndef DEF_H #define DEF_H int def_func(int a, int b); extern int def_result; #endif /* DEF_H */ |
def.c
1 2 3 4 5 6 7 8 9 |
#include "def.h" int def_result; int def_func(int a, int b) { def_result = (a + b); return def_result; } |
よくあるコンパイルエラー
ファイル分割に慣れていないと、コンパイルエラーがたくさん出てしまい、かなり苦労するかと思います。
以下に、Visual Studioでのコンパイルエラーの例と、解決するためには何を確認すれば良いのかを記載していますので、参考になれば幸いです。
C1083 includeファイルを開けません。’ヘッダファイル名’:No such file or directory
ヘッダファイルが見つからない場合のエラーです。ヘッダファイルが存在するか、ヘッダファイル名が間違っていないか、ヘッダファイルを置いているフォルダ階層が間違っていないかなどを確認しましょう。
C2065 ‘名前’: 定義されていない識別子です。
定数定義や、構造体定義、グローバル変数定義が見つからない場合のエラーです。includeしているヘッダファイルに不足はないか、ヘッダファイル内に各種定義があるか、定義名が間違っていないかなどを確認しましょう。
C2146 構文エラー: ‘;’ が、識別子 ‘変数名’ の前に必要です。
型(多くの場合は構造体)が見つからない場合のエラーです。includeしているヘッダファイルに不足はないか、ヘッダファイル内に構造体定義があるか、構造体名が間違っていないかなどを確認しましょう。
C4013 関数 ‘関数名’ は定義されていません。int 型の値を返す外部関数と見なします。
関数のプロトタイプ宣言が見つからない場合のエラーです。includeしているヘッダファイルに不足はないか、ヘッダファイル内にプロトタイプ宣言があるか、関数名が間違っていないかなどを確認しましょう。
LNK2019 未解決の外部シンボル “関数名” が関数 _main で参照されました。
関数の実体(処理内容)が見つからない場合のエラーです。関数の実体があるファイルがコンパイル対象となっているか、関数名(プロトタイプ宣言も)が間違っていないかなどを確認しましょう。
LNK2001 外部シンボル “変数名” は未解決です。
グローバル変数の実体が見つからない場合のエラーです。extern指定されていないグローバル変数があるか、その変数が記述されているファイルがコンパイル対象となっているか、グローバル変数名が間違っていないかなどを確認しましょう。
まとめ
C言語でのファイル分割のやり方について解説しました。
- C言語でファイル分割を行うには、「ヘッダファイル」を上手く使いこなす必要がある。
- ヘッダファイルには、定数や構造体の定義、関数のプロトタイプ宣言などの「前方宣言」を記述する。
- 「前方宣言」を使いたいファイルで、ヘッダファイルをincludeする。
- 二重include防止を忘れずに行うことが重要。
コメント