Menu

C言語のヘッダファイル: .h と .c、インクルードガード、複数ファイルのプログラム

C言語のプログラムを複数のファイルに分ける方法: .h に入れるもの、.c に入れるもの、二重インクルードを防ぐインクルードガード、複数ファイルのコンパイル、そして extern によるグローバル変数の共有。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

1 つの .c ファイルで済むのは、それが 2,000 行になるまでです。プログラムを複数のファイルに分ければ、それぞれを別々にコンパイルでき、他のプログラムで再利用でき、単独で読めるようになります - しかし C にはインポートの仕組みがありません。あるのは、テキストを貼り付けるプリプロセッサと、最後に断片を結合するリンカだけです。

ヘッダファイル.h)は、それらの断片のあいだの共有された契約です。実装を含まないまま、どのソースファイルにも「どこかに何が存在するか」を伝えます。

宣言と定義

この設計全体は 1 つの区別の上に成り立っています。

宣言これはどこかに存在し、その形はこうだと述べるものです。コードを生成せず、何度現れてもかまいません:

int add(int a, int b);        /* 関数の宣言(プロトタイプ) */
extern int error_count;       /* 変数の宣言 */
struct Point { int x, y; };   /* 型の定義 - ファイルごとに繰り返しても安全 */

定義はその実体を作ります。プログラム全体でちょうど 1 回だけ現れなければなりません:

int add(int a, int b) { return a + b; }   /* 関数の定義 */
int error_count = 0;                      /* 変数の定義 */

ヘッダは宣言を、ソースファイルは定義を持ちます。これを逆にすると、リンカが "multiple definition of ..." と文句を言います - 定義がヘッダに紛れ込んだことを確実に示す、唯一のエラーメッセージです。

2 ファイルのプログラム

これが最小限で役に立つ分割です。2 つの関数を宣言するヘッダ:

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int max_of(int a, int b);

#endif

それらを実装するソースファイル - 自分自身のヘッダをインクルードしている点に注目してください:

/* math_utils.c */
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int max_of(int a, int b) {
    return (a > b) ? a : b;
}

そしてそれらを使うプログラム:

/* main.c */
#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("add(3, 4)    = %d\n", add(3, 4));
    printf("max_of(3, 4) = %d\n", max_of(3, 4));
    return 0;
}

両方のソースファイルを一緒にコンパイルします:

gcc main.c math_utils.c -o app
./app

注目に値する点が 2 つあります。第一に、math_utils.c は自分自身のヘッダをインクルードしています - これは冗長ではありません。各定義がその宣言と一致しているかをコンパイラに確認させるためです。ヘッダのプロトタイプを変えて .c ファイルを直し忘れたとき、リンク時の不一致ではなくその場でエラーになります。

第二に、math_utils.hgcc のコマンドラインにありません。ヘッダはコンパイルされることはなく、#include によって .c ファイルへ貼り付けられるものです。.h をコンパイラに渡すと、はぐれたプリコンパイル済みヘッダファイルができるだけで、リンクできるコードは生まれません。

同じプログラムを 1 ファイルに詰め込んだものがこちらです。ここで実行できます:

main の前にある宣言こそ、本来の版でヘッダが供給するものです - まさにそれが、関数プロトタイプとヘッダが規模違いの同じ発想である理由です。

インクルードガード

#include はテキストを貼り付けるので、2 回貼り付けられたテキストは重複します。関数プロトタイプなら無害ですが、struct では致命的です:

/* ガードのない shapes.h */
struct Point { int x, y; };

main.cshapes.hcanvas.h の両方をインクルードし、さらに canvas.hshapes.h をインクルードしていると、コンパイラは 1 つの翻訳単位の中で struct Point が 2 回定義されているのを見て "redefinition of 'struct Point'" で止まります。実際のプロジェクトではこの連鎖が深くなりすぎて、手で追いきれなくなります。

解決策がインクルードガードです。「このヘッダはもう貼り付けられた」と記録するマクロです。

/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H

struct Point { int x, y; };
struct Point origin_point(void);

#endif /* SHAPES_H */

最初のインクルードでは SHAPES_H は未定義なので本体が残り - その途中で SHAPES_H を定義します。同じファイル内での以降のインクルードはそれが定義済みであることを見つけ、まっすぐ #endif まで飛びます。マクロ名はプロジェクト全体で一意でなければならず、パスから導いた FILENAME_H が通例です。

1 行で済む代替手段は、主要なコンパイラすべてが対応しています:

/* shapes.h */
#pragma once

struct Point { int x, y; };

#pragma once は名前の衝突に悩まされることがなく、#endif の打ち間違いで壊れることもありません。唯一の欠点は C の標準規格に入っていないことなので、変わったコンパイラでビルドしなければならないプロジェクトは #ifndef 形式を選ぶべきです。いずれにせよ、どのヘッダにも必ず 1 つ付けます - 例外はありません。他の何もインクルードしないだろうと思っているヘッダも含めてです。

ヘッダに置くべきもの

.h に入れるもの:

  • 関数プロトタイプ
  • structunionenum の定義
  • typedef 宣言
  • 共有するためのマクロ
  • 共有グローバル変数の extern 宣言
  • そのヘッダ自身が自己完結するために必要な #include

.h に入れないもの:

  • 関数の本体(意図的な static inline を除く)
  • 変数の定義 - ヘッダ内の int counter; は、それをインクルードするすべてのファイルで別々の変数を定義するか、コンパイラによってはリンクエラーになります
  • そのヘッダ自身が必要としないヘッダの #include - その依存関係を下流のすべてに押し付けることになります

「自己完結」はルールにする価値があります。ヘッダは、他の何よりも先に最初にインクルードされてもコンパイルできるべきです。shapes.hsize_t を使うなら、インクルード元がやってくれることを期待するのではなく、自分で <stddef.h> をインクルードします。

完全で、行儀のよいヘッダ:

/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H

#include <stddef.h>   /* 下で使う size_t のため */

#define MAX_NAME 64

typedef struct {
    char   name[MAX_NAME];
    int    quantity;
    double price;
} Item;

/* プログラム全体で共有し、inventory.c で 1 度だけ定義する */
extern int item_count;

void   inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);

#endif /* INVENTORY_H */

extern によるグローバル変数の共有

グローバル変数はちょうど 1 つの .c ファイルで定義され、それ以外のすべての場所で宣言されなければなりません。その宣言を作るのが extern です:

/* inventory.h  - 宣言、記憶域は持たない */
extern int item_count;
/* inventory.c  - ただ 1 つの定義 */
#include "inventory.h"
int item_count = 0;
/* main.c - ヘッダ経由で使う */
#include <stdio.h>
#include "inventory.h"

int main(void) {
    printf("%d 個のアイテム\n", item_count);
    return 0;
}

ヘッダから extern を外すと、インクルードするすべてのファイルが自分自身の item_count を定義してしまいます。よくて "multiple definition" のリンクエラー、悪ければ独立した 2 つのカウンタができあがります。

その逆の要求も同じくらいよくあります。1 つの .c ファイルの中だけに閉じておきたい変数やヘルパー関数です。ファイルスコープの static がそれを実現します - 名前に内部リンケージを与え、リンカから、つまり他のすべてのファイルから見えなくします:

/* inventory.c */
static Item storage[256];          /* このファイル専用 */
static int  find_slot(const char *name);   /* プライベートなヘルパー */

2 つのファイルがそれぞれ static int counter; を持っても衝突しません。これは C 版のプライベートメンバーであり、まず手を伸ばすべき既定の選択肢です - 他のファイルが本当に必要とするものだけをヘッダに置きましょう。

より大きなプログラムのコンパイル

すべてのファイルを並べる方法は動きますが遅いです。毎回すべてのファイルが再コンパイルされるからです:

gcc main.c inventory.c report.c -o app

スケールする形は、各ソースをオブジェクトファイルへコンパイルしてからリンクするやり方です:

gcc -c main.c        # main.o ができる
gcc -c inventory.c   # inventory.o ができる
gcc -c report.c      # report.o ができる
gcc main.o inventory.o report.o -o app

こうすれば report.c を変えたときに必要なのは gcc -c report.c と再リンクだけです。まさにこの帳簿づけを自動化するのが make です:

app: main.o inventory.o report.o
	gcc main.o inventory.o report.o -o app

%.o: %.c
	gcc -Wall -Wextra -c $< -o $@

ヘッダがサブディレクトリにあるなら、-Iinclude でそれを山かっこの検索パスに追加できます。

エラーとその意味

2 つの失敗パターンは、どの段階が生んだものかが分かれば簡単に見分けられます。

"undefined reference to 'add'" - リンカのエラーです。宣言は見つかったが定義は見つからなかった、ということ。.c ファイルをコマンドラインに並べ忘れたか、その関数が static か、2 か所のどちらかで名前を打ち間違えています。

"multiple definition of 'item_count'" - これもリンカのエラーで、その鏡像です。定義がヘッダに入り込んだか、2 つのソースファイルに入っています。1 つの .c へ移し、ヘッダには extern 宣言を残しましょう。

"redefinition of 'struct Item'" - コンパイラのエラーで、ヘッダが 1 つのファイルに 2 回貼り付けられたという意味です。インクルードガードを追加してください。

"implicit declaration of function 'add'" - コンパイラの警告(C99 以降のモードではエラー)で、プロトタイプが一度も見えていないという意味です。#include を忘れたか、そのヘッダが宣言していません。

プログラムが複数ファイルにまたがるようになると、次にたいてい欲しくなるのは、プラットフォームごと・ビルド種別ごとにコンパイルする内容を変えることです - それが条件付きコンパイルです。

よくある質問

.h ファイルには何を、.c ファイルには何を書きますか?

ヘッダには宣言を置きます - 関数プロトタイプ、structtypedef の定義、enum、マクロ、そして共有グローバル変数の extern 宣言です。.c ファイルには定義を置きます - 関数の本体と実体としての変数です。目安: ヘッダは「何が存在するか」を語り、ソースファイルは「それが何をするか」を語ります。

インクルードガードとは何で、なぜ必要なのですか?

#include はテキストを貼り付けるので、ヘッダを 2 回インクルードすると中身が 2 回貼り付けられます - その中の構造体や typedef がすべて再定義され、コンパイルに失敗します。インクルードガードはヘッダを #ifndef MYHEADER_H / #define MYHEADER_H / #endif で包み、2 回目のインクルードではマクロがすでに定義されているので本体を読み飛ばすようにします。

複数のファイルからなる C言語のプログラムはどうコンパイルしますか?

すべての .c ファイルをコマンドラインに並べます: gcc main.c math_utils.c -o app.h ファイルはそこに書いてはいけません - ヘッダは #include によって貼り付けられるもので、単独でコンパイルするものではありません。大きなプロジェクトではオブジェクトファイルへコンパイル(gcc -c main.c)してからリンクします。それを自動化するのが Makefile です。

#pragma once と #ifndef のインクルードガード、どちらを使うべきですか?

どちらでも動きます。#pragma once は 1 行で済み、名前の衝突も起こりえず、主要なコンパイラはすべて対応しています - ただし C の標準規格には入っていません。#ifndef/#define/#endif の形は標準で、どこでも動きます。プロジェクト内ではどちらかに統一しましょう。移植性を最大にしたいなら #ifndef 形式を選んでください。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める