C言語の学習を進めていくと、1つのファイルだけでプログラムを書くやり方では、だんだん管理が大変になってきます。
そこで出てくるのが、複数のファイルに処理を分けて書くという方法です。
ただし、ファイルを分けるだけでは他のファイルの関数をそのまま呼び出すことはできません。
鍵となるのが「関数のプロトタイプ宣言」と「ヘッダファイル(.h)」です。
この記事では、C言語初心者の方がつまずきやすい複数ファイルとプロトタイプ宣言の関係を、具体例とコード付きで丁寧に解説します。
複数ファイルで関数を使う基本
1つの.cだけでは足りなくなる理由
最初にC言語を学ぶときは、すべてのコードを1つの.cファイルに書くことが多いです。
例えばmain.cの中にmain関数とその他の関数を全部書く、という形です。
しかし、プログラムが少しずつ大きくなってくると、1つのファイルに全部の処理を詰め込む方法には、次のような問題が出てきます。
- コードの行数が増えすぎて、どこに何が書いてあるか分かりにくくなる
- 関連する処理ごとに分けて考えることが難しくなる
- 他のプロジェクトで同じ関数を再利用しにくくなる
特に、再利用性の面で大きな問題になります。
例えば、配列をソートする関数や、文字列を整形する関数などを別のプログラムでも使いたいと思ったとき、1つの巨大なファイルから必要な部分だけを抜き出すのは面倒です。
そこで、「役割ごとにファイルを分けて書く」という考え方が重要になります。
複数ファイルに分けるメリットと注意点
複数の.cファイルにコードを分割すると、次のようなメリットがあります。
1つ目は、見通しの良さです。
例えば、入出力関連の処理はio.cに、数値計算はmathutil.cに、といった形で分けることで、読みやすくなります。
2つ目は、再利用のしやすさです。
よく使う関数をutil.cのようなファイルにまとめておけば、別のプロジェクトでもそのファイルを追加するだけで利用できます。
3つ目は、複数人での開発がしやすくなる点です。
ファイルごとに担当を分けて作業できるため、同時に作業しても衝突が少なくなります。
ただし、複数ファイルに分けるときには注意点もあります。
特に重要なのは、「他のファイルにある関数をどうやって呼び出すか」という問題です。
Cコンパイラは、それぞれの.cファイルを別々にコンパイルします。
そのため、コンパイルの時点で「この名前の関数が、どんな引数でどんな型を返すのか」を知っておく必要があります。
この情報を伝えるのが関数のプロトタイプ宣言です。
main.cと別ファイルの関数を呼び出す流れ
複数ファイルで関数を使うときの基本的な流れを、簡単な例でイメージしてみます。
ここでは、main.cからadd.cに定義したadd関数(2つの整数を足す関数)を呼び出す、とします。
全体の構成は次のようになります。
- main.c … メインの処理を書いたファイル
- add.c … 足し算を行う
add関数の本体を書くファイル - add.h …
add関数のプロトタイプ宣言を書くヘッダファイル
この3つが連携して動きます。
1つ目に、add.cにはadd関数の本体(定義)を書きます。
2つ目に、add.hにはadd関数のプロトタイプ宣言を書きます。
3つ目に、main.cでは#include "add.h"として、プロトタイプ宣言を読み込みます。
最後に、コンパイル時にはmain.cとadd.cを一緒にコンパイルし、リンクします。
このときプロトタイプ宣言が正しく共有されているかどうかが、とても重要になります。
関数のプロトタイプ宣言とは
プロトタイプ宣言の書き方と意味
関数のプロトタイプ宣言とは、コンパイラに対して「このような関数がどこかに存在します」と伝えるための宣言です。
書式は、関数定義の先頭部分から中身のブロック{ ... }を取り除き、最後にセミコロン;を付けた形になります。
例えば、次のような関数定義があるとします。
int add(int a, int b) {
return a + b;
}
この関数のプロトタイプ宣言は、次のようになります。
int add(int a, int b);
プロトタイプ宣言で重要なのは、戻り値の型と引数の型です。
引数名は書いても書かなくても構いませんが、初心者のうちは関数定義と同じように書いておいた方が分かりやすいです。
この宣言を見たコンパイラは、「addという名前の関数があって、int型を2つ受け取り、int型を返す」という情報を理解します。
これにより、main関数からaddを呼び出すときに、引数や戻り値の型が正しいかどうかをチェックできます。
プロトタイプ宣言がないと起きる問題
プロトタイプ宣言がない状態で、別のファイルにある関数を呼び出そうとすると、コンパイラはその関数の正しい情報を持ちません。
そのため、古い仕様では「暗黙のint宣言」として扱うこともありましたが、現在のCコンパイラではエラーになることがほとんどです。
例えば、プロトタイプ宣言なしでaddを呼び出そうとするコードを考えます。
#include <stdio.h>
// addのプロトタイプ宣言を書いていない状態
int main(void) {
int result;
// ここでaddを呼び出そうとする
result = add(10, 20);
printf("result = %d\n", result);
return 0;
}
このmain.cだけをコンパイルすると、多くの場合、「暗黙の宣言」や「宣言されていない関数の使用」に関するエラーや警告が出ます。
なぜなら、コンパイラはaddという名前を初めて見た時点では、その戻り値や引数の情報を知らないからです。
コンパイル時に関数の情報を知っておく必要があるので、プロトタイプ宣言が必須になります。
「未定義の参照」エラーとプロトタイプの関係
複数ファイルでプログラムを作るとき、よく出てくるエラーの1つが「未定義の参照」(undefined reference)です。
これは主にリンク時に発生するエラーです。
このエラーは、コンパイルの後の段階で、「宣言されているのに、実際の本体(定義)が見つからない」場合に出ます。
つまり、次の2つがずれている状態です。
- プロトタイプ宣言(「こういう関数があります」という約束)
- 関数定義(実際の中身)
例えば、main.cで次のように宣言して使っていたとします。
int add(int a, int b); // プロトタイプ宣言
int main(void) {
int result = add(1, 2);
return 0;
}
しかし、コンパイルとリンクのときにadd.cを一緒に指定し忘れた場合、リンカはadd関数の本体を見つけられません。
その結果、次のようなエラーが出ることがあります。
- undefined reference to
add
このとき、プロトタイプ宣言そのものは正しいので、コンパイルは通ってしまいます。
しかし、リンク時に「宣言したのに本体がない」と怒られるわけです。
このように、プロトタイプ宣言は「存在するはず」という約束であり、関数定義はその約束を実際に守るものだとイメージしておくと理解しやすくなります。
.cファイルと.hヘッダファイルの役割
.cファイル(実装ファイル)に書くもの
.cファイルは、関数の実際の中身(処理)を書く場所です。
一般的に、「実装ファイル」と呼ばれます。
例えば、足し算と引き算の関数をまとめたcalc.cは、次のようになります。
// calc.c : 計算に関する関数の実装ファイル
#include "calc.h" // 後で作るヘッダファイルをインクルードするとよい
// 2つの整数を足し算する関数
int add(int a, int b) {
return a + b;
}
// 2つの整数を引き算する関数
int sub(int a, int b) {
return a - b;
}
.cファイルには、主に次のものを書きます。
- 関数の定義(本体)
- 必要な
#include文 - 必要に応じて、ファイル内だけで使う
staticな関数や変数
外から呼び出してもらいたい関数は、この.cファイルに定義し、後で紹介するヘッダファイルでプロトタイプ宣言を公開します。
.hファイル(ヘッダファイル)に書くもの
.hファイルは、他のファイルに知らせたい情報(宣言)を書く場所です。
一般的に、「ヘッダファイル」と呼ばれます。
例えば、先ほどのcalc.cに対応するcalc.hは、次のようになります。
// calc.h : 計算に関する関数のヘッダファイル
#ifndef CALC_H // インクルードガード(後で詳しく説明します)
#define CALC_H
// 2つの整数を足し算する関数のプロトタイプ宣言
int add(int a, int b);
// 2つの整数を引き算する関数のプロトタイプ宣言
int sub(int a, int b);
#endif // CALC_H
.hファイルには、主に次のようなものを書きます。
- 他のファイルから使える関数のプロトタイプ宣言
- 構造体や列挙型の定義
#defineによるマクロ定義- 外部変数の宣言(
extern int g_value;のような形)
実際の処理(関数の中身)は書かない点が重要です。
処理は必ず.cファイル側に書きます。
関数プロトタイプをヘッダにまとめる理由
関数のプロトタイプ宣言は、確かに.cファイルの先頭に直接書くこともできます。
しかし、現実的にはヘッダファイル(.h)にまとめて書くのが一般的です。
その理由はいくつかあります。
まず、複数の.cファイルから同じ関数を使いたい場合、それぞれの.cファイルに同じプロトタイプ宣言をコピペして書くのは非効率で、ミスの原因にもなります。
ヘッダファイルにまとめておけば、#includeするだけで同じ宣言を共有できます。
次に、宣言と定義の不一致を防ぎやすいという利点があります。
関数の引数や戻り値の型を変えたとき、ヘッダファイルだけを修正すれば、コンパイル時に他のファイルとの不一致が見つかります。
さらに、モジュールのインターフェースを明確にできるという点もあります。
ヘッダファイルを見るだけで「このモジュールはどんな関数を外部に提供しているのか」が分かるため、設計の整理にも役立ちます。
ヘッダファイルのインクルード方法と注意点
ヘッダファイルを使うには、#includeディレクティブで読み込む必要があります。
自分で作ったヘッダファイルをインクルードするときは、二重引用符"..."を使います。
例えば、main.cでcalc.hを使う場合は次のように書きます。
#include <stdio.h> // 標準ライブラリのヘッダは < > で囲む
#include "calc.h" // 自分で作ったヘッダは " " で囲む
ここでの注意点として、インクルードの順番があります。
一般的には、標準ライブラリのヘッダを< > で先にインクルードし、その後に自作のヘッダを" "でインクルードする形がよく使われます。
また、1つのヘッダファイルが別のヘッダファイルをインクルードする場合など、同じヘッダが何度もインクルードされてしまうことがあります。
これを防ぐために使う仕組みがインクルードガードで、先ほどのcalc.hのように#ifndef〜#define〜#endifで囲む方法が一般的です。
複数ファイルで関数を共有する実践ステップ
ここからは、実際に複数ファイルを使って関数を共有する具体的な例を、ステップごとに解説します。
C言語初心者の方でも、そのまま真似しながら試せるように、ファイル構成からコンパイル方法まで順番に説明します。
main.cから別ファイルの関数を呼び出す手順
今回の例では、次のような構成を作ります。
- main.c … ユーザーから2つの整数を入力し、合計を表示する
- calc.c … 足し算を行う
add関数を定義する - calc.h …
add関数のプロトタイプ宣言をまとめたヘッダ
全体の流れは次のようになります。
calc.cにadd関数の本体を書くcalc.hにadd関数のプロトタイプ宣言を書くmain.cで#include "calc.h"としてプロトタイプ宣言を読み込む- コンパイラで
main.cとcalc.cを一緒にコンパイルし、リンクする
これを順番に見ていきます。
関数定義を別の.cに分ける具体例
まず、calc.cに足し算をするadd関数の本体を書きます。
// calc.c : 足し算を行う関数の実装ファイル
#include "calc.h" // 自分自身のヘッダをインクルードしておくと、宣言と定義の不一致を検出しやすくなります
// 2つの整数を足し合わせて、その結果を返す関数
int add(int a, int b) {
int result = a + b; // 計算結果を一時変数に保存
return result; // 計算結果を呼び出し元に返す
}
ここでは、add関数が「2つのint型の引数を取り、int型の結果を返す」関数であることが分かります。
実際の処理はとても簡単ですが、重要なのは関数定義をメインとは別の.cファイルに書いている点です。
次に、main.cを見てみましょう。
// main.c : ユーザーから2つの整数を入力し、その合計を表示する
#include <stdio.h> // printf, scanfのための標準入出力ヘッダ
#include "calc.h" // add関数のプロトタイプ宣言を含むヘッダ
int main(void) {
int x, y;
int sum;
// ユーザーから2つの整数を入力してもらう
printf("1つ目の整数を入力してください: ");
scanf("%d", &x);
printf("2つ目の整数を入力してください: ");
scanf("%d", &y);
// 別ファイル(calc.c)にあるadd関数を呼び出して、合計を計算する
sum = add(x, y);
// 計算結果を表示する
printf("合計は%dです。\n", sum);
return 0; // 正常終了
}
このmain.cでは、add関数の中身は一切書かれていません。
それでもプロトタイプ宣言をcalc.hから読み込むことで、コンパイラはadd関数を正しく認識してくれます。
共通ヘッダ(.h)を作ってプロトタイプ宣言を共有
ここで重要になるのが、calc.hです。
calc.hには、他のファイルから使ってほしい関数のプロトタイプ宣言を書きます。
// calc.h : 足し算を行う関数のプロトタイプ宣言をまとめたヘッダファイル
#ifndef CALC_H // インクルードガードの開始
#define CALC_H
// 2つの整数を足し合わせて、その結果を返す関数のプロトタイプ宣言
int add(int a, int b);
#endif // CALC_H インクルードガードの終了
このヘッダファイルを作ることで、どの.cファイルからでも#include "calc.h"と書くだけでadd関数を使えるようになります。
ここまでの3つのファイルをまとめて整理すると、次のようになります。
main.c … メイン処理(ユーザー入力と結果表示)
calc.c … add関数の本体
calc.h … add関数のプロトタイプ宣言
複数回インクルードを防ぐインクルードガード
先ほどのcalc.hには、次のような記述がありました。
#ifndef CALC_H
#define CALC_H
// プロトタイプ宣言など
#endif // CALC_H
これはインクルードガードと呼ばれる仕組みで、同じヘッダファイルが複数回インクルードされてしまうことを防ぐために使います。
Cの#includeは、指定されたファイルの中身をその場に「コピペ」するような動作をします。
そのため、同じヘッダファイルが何度もインクルードされると、同じプロトタイプ宣言や構造体定義が繰り返し現れてしまい、
- 再定義エラー
- 型の再宣言エラー
のようなコンパイルエラーの原因になります。
インクルードガードは、一度だけ読み込まれるようにするための「安全装置」です。
一般的な書き方は、次のようになります。
#ifndef ファイルごとのユニークな名前
#define ファイルごとのユニークな名前
// このヘッダファイルの中身
#endif
この「ユニークな名前」には、通常ファイル名を大文字にし、拡張子のドットをアンダースコアに変えたものなどを使います。
例えばcalc.hならCALC_Hのようにします。
コンパイルとリンクのやり方
最後に、これらのファイルを実際にコンパイルして実行ファイルを作る手順を確認します。
ここでは、代表的なコンパイラであるgccを例に説明します。
1つのコマンドでまとめてコンパイル・リンクする場合
もっとも簡単な方法は、main.cとcalc.cを同時に指定してコンパイルするやり方です。
// これはCコードではなく、シェル(コマンドプロンプト)で実行するコマンド例です
// gccコンパイラを使ってmain.cとcalc.cをまとめてコンパイルし、実行ファイルa.out(Windowsではa.exe)を作成します。
// 実際の端末では、次のように入力してEnterキーを押します:
//
// gcc main.c calc.c
//
// 成功すれば、同じフォルダに実行ファイルが生成されます。
実際のコマンドは次のようになります(これはコードブロックとして示しますが、C言語ではなくシェルコマンドです)。
gcc main.c calc.c
このコマンドが行うことは、次の2段階です。
- コンパイル
main.cとcalc.cをそれぞれオブジェクトファイル(main.o、calc.o)に変換する
- リンク
main.oとcalc.oを結合し、実行ファイル(a.outやa.exe)を作る
実行すると、次のような動きをします。
1つ目の整数を入力してください: 10
2つ目の整数を入力してください: 25
合計は35です。
コンパイルとリンクを分けて行う場合
少し発展的なやり方として、コンパイルとリンクを分けて行う方法も紹介します。
大きなプロジェクトでは、こうした分割コンパイルが一般的です。
まず、それぞれの.cファイルをオブジェクトファイルにコンパイルします。
gcc -c main.c // main.c から main.o を作る
gcc -c calc.c // calc.c から calc.o を作る
その後、オブジェクトファイル同士をリンクします。
gcc main.o calc.o -o myprog
これでmyprog(Windowsではmyprog.exe)という実行ファイルが生成されます。
まとめ
C言語で複数ファイルから関数を使うには、関数のプロトタイプ宣言とヘッダファイルの役割を正しく理解することが大切です。
関数の本体は.cファイルに書き、他のファイルから使いたい関数のプロトタイプ宣言は.hファイルにまとめます。
そして、必要な.cファイルをすべて指定してコンパイル・リンクすることで、main.cから別ファイルの関数を安全に呼び出せるようになります。
今回の例を手元で実際に動かしながら、プロトタイプ宣言と複数ファイルの流れを体で覚えていくと理解が深まりやすいです。
