C言語の正しいヘッダファイルの書き方

最近、仕事でC言語での組み込み系の開発に携わっています。
開発中のコードを眺めていると、ヘッダファイル内にstatic関数のプロトタイプ宣言を記述していたり、ヘッダファイル内で不必要に他のヘッダファイルをインクルードしているなど、ヘッダファイルの書き方が分かっていないと思われる箇所が多々見られました。
実際、C言語の入門書でもヘッダファイルの書き方を詳しく説明しているものは、僕の知っている限りでは存在しないので、C言語を使っていてもヘッダファイルの正しい書き方を知らない人が少なくないのではないかと思われます。
そこで、このエントリでは、C言語のヘッダファイルの書き方について、僕が知っているテクニックをまとめてみました。

インクルードガードを書く

ヘッダファイルファイルで他のヘッダファイルをインクルードしていると、いつの間にか同じヘッダファイルを2回インクルードしてしまうことがあります。
例えば、foo.h、bar.h、baz.hの3つのヘッダファイルがあって、それぞれのヘッダファイルが次の例のようになっているとします。

/* foo.h */
typedef struct
{
  int a;
} Foo;
/* bar.h */
#include "foo.h"

typedef struct
{
  Foo foo;
} Bar;
/* baz.h */
#include "foo.h"
#include "bar.h"

typedef struct
{
  Foo foo;
  Bar bar;
} Baz;

この場合、baz.h内で、foo.hが直接のインクルードとbar.h経由のインクルードにより2回インクルードされているため、重複定義となりコンパイルエラーが発生してしまいます。
これを避けるために、ヘッダファイル内に次の例のようなマクロを記述します。

/* foo.h */
#ifndef FOO_H
#define FOO_H

/* foo.h の中身 */

#endif

このマクロをインクルードガードと呼びます。インクルードガードを書くことによって、2回目以降のインクルードではファイルの中身が展開されなくなるので、定義の重複によるコンパイルエラーを避けることができます。

このテクニックは非常によく使うので、エディタで自動的にインクルードガードを挿入してくれるようにしておくと、非常に便利だと思われます。
Emacsの場合は、http://d.hatena.ne.jp/higepon/20080731/1217491155に詳細な設定方法が紹介されています。

staticな関数やグローバル変数の宣言はヘッダファイル内には書かない

これは当たり前のことかと思いますが、この原則が守られてないソースが実際に存在したため、あえて強調しておきます。

static指定子の意味は、static指定されたグローバル変数や関数のスコープをそのソースファイル内に限定するということです。
(関数内のローカルstatic変数の意味はまた別)
ですので、それらをヘッダファイルに書いて外部に公開するというのは意味的に考えて明らかに間違いです。

また、static変数をヘッダファイルに記述してしまうと、そのヘッダをインクルードしたソースごとに別々の変数の実体が作成されてしまうため、おそらく意図した挙動にはならないと思われます。

グローバル変数をextern宣言する

次の例のようにヘッダファイル内でグローバル変数を定義してしまうと、そのヘッダファイルを複数のソースで使用した場合に同名のグローバル変数の定義が複数のソースファイル内で行われてしまうので、リンク時にエラーが発生してしまいます。

/* sub.h */
#ifndef SUB_H
#define SUB_H

int global_var = 0;

int sub(void);

#endif
/* sub.c */
#include "sub.h"

int sub(void)
{
  return global_var;
}
#include <stdio.h>
#include "sub.h"

int main(void)
{
  printf("global_var = %d\n", global_var);
  printf("sub() = %d\n", sub());
  return 0;
}

この場合、sub.h内のグローバル変数の宣言にextern指定子を追加します。こうしておくことで、グローバル変数の定義が他の場所で行われていることをコンパイラに知らせることができます。そして、ソースファイル側でexternをつけないでグローバル変数を定義します。

/* sub.h */
#ifndef SUB_H
#define SUB_H

extern int global_var;

int sub(void);

#endif
/* sub.c */
#include "sub.h"

int global_var = 0;

int sub(void)
{
  return global_var;
}

ヘッダファイル内では他のヘッダファイルのインクルードを最低限に留める

makeなどのビルドシステム(個人的にはRakeを使っています)でヘッダファイルの依存関係を自動で解決するようにしていると、ヘッダファイルが変更された場合、そのヘッダをインクルードしているソースが全て再コンパイルされます。
そのため、ヘッダファイル内で他のヘッダを不必要にインクルードしていると、ヘッダファイルを更新した際に、大量のソースコードを再コンパイルされ、コンパイル時間が増大してしまいます。
そのため、ヘッダファイル内では他のヘッダファイルのインクルードを最低限に留め、他のヘッダファイルはソースファイル内でインクルードします。

ヘッダファイル内でのインクルードを減らすためのテクニックとして、構造体の前方宣言があります。

例えば、hoge.hとfuga.hの2つのヘッダファイルがあり、hoge.hでHoge構造体が定義され、fuga.hのFuga構造体でHoge構造体を使用していたとします。

/* hoge.h */
#ifndef HOGE_H
#define HOGE_H

typedef struct hoge_t
{
  int a;
} Hoge;

#endif
/* fuga.h */
#ifndef FUGA_H
#define FUGA_H

#include "hoge.h"

typedef struct fuga_t
{
  Hoge* hoge;
} Fuga;

#endif

この例の場合、Fuga構造体のメンバはHoge構造体の実体ではなくポインタなので、Fuga構造体を定義するだけならばHoge構造体の具体的な定義は必要ありません。
そのため、hoge.hをインクルードする代わりに、次の例のように前方宣言を使って書くことが出来ます。

/* fuga.h */
#ifndef FUGA_H
#define FUGA_H

struct hoge_t; /* 前方宣言 */

typedef struct fuga_t
{
  struct hoge_t* hoge;
} Fuga;

ちなみに、僕の手元の環境(i686-apple-darwin10-gcc-4.2.1)では前方宣言がなくてもコンパイルできたので、前方宣言が必要というのは僕の勘違いかもしれません。

ヘッダファイルの依存を減らすために構成を工夫する

ある構造体が定義されているヘッダを多くの箇所で使用している場合、その構造体を修正する際に、その構造体を使用している多くのソースを再コンパイルする必要性がでてきます。
この問題は、ヘッダファイルを公開用と非公開用の2つに分けて、構造体の定義を非公開用ヘッダに記述するという運用を行うことによって解決することが出来ます。

具体的には、次の例のように各ファイルを記述します。

/* foo.h */
#ifndef FOO_H
#define FOO_H

/* foo.hでは構造体の前方宣言だけを行って、構造体の定義は行わない */
struct foo_t;
typedef struct foo_t Foo;

/* Foo構造体へのアクセスは全て専用のAPI関数経由で行うようにする。
   これらの関数では、Fooのポインタを第一引数に取る。
*/
Foo* Foo_alloc();
void Foo_do_something(Foo* obj, int arg1, int arg2);
void Foo_free(Foo *obj);

#endif
/* private/foo_p.h */
#ifndef FOO_P_H
#define FOO_P_H

#include "foo.h"
/* Foo構造体の定義は非公開ヘッダ内に記述する */
struct foo_t
{
  int a;
  int b;
};

#endif
/* foo.c */
#include "private/foo_p.h"

/* foo.cでは非公開ヘッダをインクルードしているが、
   その他のソースでは公開ヘッダのみをインクルードする。
*/

/* Foo_alloc等の関数のコードをこのファイルに書く */

このような構成にすることによって、Foo構造体の修正を行っても、foo.hをインクルードしているソースの再コンパイルは必要なくなります。
また、構造体へのアクセスを全て専用のAPI関数経由で行うことを強制することができるので、カプセル化を促進することができるという別のメリットもあります。