はじめに
Cのプロジェクトが大きくなってくると、「これはどのファイルに書くべきか」という判断が増えてきます。特にヘッダファイルの設計は、チームで開発する場合に認識がずれやすいポイントです。
「とりあえず共通で使いたいものはヘッダに書く」という感覚で進めると、いつの間にかビルドエラーや多重定義が発生して原因究明に時間を取られることがあります。
今回はヘッダファイルに書いていいものと書いてはいけないものを整理した上で、多重インクルードの防止と条件付きコンパイルまで見ていきます。前回扱ったマクロやインライン関数がどこに置くべきかも、ここで自然に整理されます。
ヘッダファイルに書いていいものと書いてはいけないもの
ヘッダファイルの役割は「複数のソースファイルで共用する宣言や定義を一か所にまとめる」ことです。ポイントは「宣言」であって「実体」ではない、という点です。
書いていいもの
#define OFF 0 /* マクロ */
#define ON 1
typedef unsigned char UB; /* 型宣言 */
struct Point { /* 変数定義をともなわない構造体の宣言 */
int x;
int y;
};
extern int g_counter; /* 外部変数のextern宣言 */
int getkey(int sw); /* 関数のプロトタイプ宣言 */
Inline UB sil_reb_mem(VP mem) /* インライン関数の定義(Inline・VPはTOPPERS固有の定義) */
{
return(*((volatile UB *) mem));
}
書いてはいけないもの
/* 関数の定義 ― ソースファイルに書く */
int getkey(int sw) {
...
}
/* 外部変数の定義 ― ソースファイルに書く */
int g_counter = 0;
なぜ関数の定義と外部変数の定義をヘッダに書いてはいけないのか。理由は「実体(メモリに割り付けられるもの)」だからです。ヘッダファイルは複数のソースファイルからインクルードされるため、実体をヘッダに書くと各ソースファイルでそれぞれ実体が生成され、リンク時に多重定義エラーになります。
インライン関数が例外的にヘッダに書けるのは、static inline(またはTOPPERSの Inline マクロ)として定義することで、各翻訳単位にインライン展開され独立したリンク対象の実体を持たないからです。
インクルードガード ― 多重インクルードを防ぐおまじない
ヘッダファイルは複数のファイルからインクルードされます。インクルードの連鎖が複雑になると、同じヘッダファイルが複数回読み込まれることがあります。
sil_akih8_3069f.h
├── ---.h
│ └── -?-.c
└── ???.h
└── -?-.c ← 同じ -?-.c から2回インクルードされる
同じヘッダが2回読み込まれると、マクロや型宣言が重複してコンパイルエラーになります。これを防ぐのがインクルードガードです。
#ifndef SIL_AKIH8_3069F_H_
#define SIL_AKIH8_3069F_H_
/* ヘッダファイルの中身 */
#endif /* SIL_AKIH8_3069F_H_ */
仕組みはシンプルです。最初にインクルードされたとき SIL_AKIH8_3069F_H_ が未定義なので #ifndef が真になり、中身が読み込まれます。同時に #define でそのマクロを定義します。2回目以降は SIL_AKIH8_3069F_H_ がすでに定義済みなので #ifndef が偽になり、中身がスキップされます。
ガード用のマクロ名はファイル名をもとに付けるのが一般的です。大文字にして . を _ に置き換えるルールが基本です。先頭にアンダースコアを付けるスタイルも見かけますが、C標準(ISO C)ではアンダースコア始まりの識別子は処理系予約済みのため、先頭は大文字英字から始める方が安全です。
現代のコンパイラでは #pragma once という一行で同じことができる場合もありますが、標準Cの機能ではないのでプロジェクトの方針に従って使い分けてください。
条件付きコンパイル ― デバッグコードの管理と設定の切り替え
#ifdef や #ifndef はインクルードガード以外にも使えます。代表的な使い方が、デバッグコードの管理です。
#define DEBUG /* デバッグ時に宣言する */
/* ... */
#ifdef DEBUG
syslog_1("timer_task: base_time = %d", base_time); /* TOPPERS固有のログ出力関数(printf相当) */
#endif
/* ... */
#ifdef DEBUG
syslog_1("device: p4 = %x", p4);
#endif
#define DEBUG がある場合は #ifdef DEBUG ~ #endif の中身がコンパイルされ、ない場合はスキップされます。リリースビルド時は #define DEBUG の行をコメントアウトするか削除するだけで、デバッグ用のログ出力をまとめて除去できます。
ソースコードに直接書かず、コンパイル時のオプションで定義する方法もあります。
gcc -DDEBUG main.c
こうすることでソースコードを変更せずにデバッグビルドとリリースビルドを切り替えられます。ビルドスクリプトやMakefileで管理するのが一般的です。
デバッグ以外にも、ターゲットのハードウェアや機能の有無によってコードを切り替える用途にも使えます。
#ifdef USE_FEATURE_A
/* 機能Aのコード */
#else
/* 機能Aなしのコード */
#endif
複数の条件が必要な場合は #elif も使えます。
#ifdef TARGET_H8
/* H8向けのコード */
#elif defined TARGET_ARM
/* ARM向けのコード */
#else
#error "ターゲットが未定義です"
#endif
#error を使うと、条件が満たされない場合にコンパイルエラーを意図的に発生させることができます。設定漏れを早期に検出したいときに便利です。
実務での注意点・よくある落とし穴
インクルードガードを付け忘れる
新しいヘッダファイルを作るたびにインクルードガードを付けるのは基本ですが、急いでいるときに忘れがちです。プロジェクトのヘッダファイルテンプレートにあらかじめインクルードガードを含めておくと付け忘れを防げます。
条件付きコンパイルのネストが深くなりすぎる
#ifdef の中に #ifdef を重ねていくと、どの #endif がどの #ifdef に対応するのかわかりにくくなります。コメントで対応関係を明示する習慣をつけておくと読みやすくなります。
#ifdef TARGET_H8
/* ... */
#ifdef USE_FEATURE_A
/* ... */
#endif /* USE_FEATURE_A */
#endif /* TARGET_H8 */
デバッグコードを消し忘れる
#ifdef DEBUG で囲んでいれば安心、と思いがちですが、#define DEBUG をコメントアウトし忘れたままリリースビルドしてしまうケースがあります。ビルドスクリプトでデバッグフラグの管理を一元化しておくのが確実です。
ヘッダファイルに実体を書いてしまう
チームに新しいメンバーが加わったときに起きやすいミスです。「共通で使いたいから」という理由でヘッダに関数の定義や変数の定義を書いてしまい、リンク時に多重定義エラーが出る、というパターンは定番のトラブルです。レビューで早めに気づける体制を作っておくとよいです。
まとめ
ヘッダファイルの設計と条件付きコンパイルは、プロジェクトが大きくなるほど重要になってきます。
- ヘッダファイルには宣言を書き、実体(関数の定義・変数の定義)は書かない
- インクルードガードはすべてのヘッダファイルに付ける
- 条件付きコンパイルでデバッグコードやターゲット依存コードを管理する
- ビルドフラグはビルドスクリプトで一元管理する
次回はビット操作を取り上げます。レジスタ制御の現場でよく使うイディオムを整理していきます。