6日でできる 新C言語入門|複数のヘッダーファイルへの分割

大きくなったプログラムは、関心ごとごとに分けると見通しがよくなる。
複数のヘッダーファイルを使って、関数・画面表示・共有データを安全に整理しよう。

C言語でソースコード分割を学ぶと、最初はmain.c、multiply.h、multiply.cのように、1つのヘッダーファイルと1つのソースファイルを組み合わせる形から始めることが多いです。

この形だけでも、main.cにすべての関数を書き込むよりはかなり整理しやすくなります。しかし、プログラムがさらに大きくなると、1つのヘッダーファイルにすべての関数宣言や定数、型情報を詰め込むのは管理しにくくなります。

たとえば、計算処理、画面表示、設定値、ファイル入出力など、役割の違う処理をすべて1つのヘッダーファイルにまとめると、内容がどんどん増えてしまいます。そうなると、どの関数がどの機能に関係しているのか分かりにくくなり、修正したときの影響範囲も広がりやすくなります。

そこで役立つのが、複数のヘッダーファイルへの分割です。

複数のヘッダーファイルに分けるとは、計算用の宣言はmathops.h、画面表示用の宣言はscreen.h、設定値はconfig.hのように、関心ごとごとにヘッダーファイルを分けて管理することです。

このように分けることで、必要なファイルだけを読み込めるようになり、プログラムの構造が分かりやすくなります。また、複数人で開発する場合も、担当範囲を分けやすくなります。

この記事では、複数のヘッダーファイルに分ける理由、ヘッダーファイルとソースファイルの対応、externを使ったグローバル変数の共有方法、分割前と分割後のサンプル、ビルド時にコンパイラやリンカがどのように働くのかを、順番にやさしく解説していきます。

1つのヘッダーファイルだけでは管理しにくくなる

小さなプログラムでは、ヘッダーファイルが1つだけでも問題なく管理できます。

しかし、関数や定数が増えてくると、1つのヘッダーファイルに多くの情報が集まりすぎてしまいます。

たとえば、次のような内容が1つのヘッダーファイルに混ざっていると、だんだん読みにくくなります。

内容
計算用の関数宣言加算、減算、平均計算など
表示用の関数宣言結果表示、区切り線表示など
設定値最大値、表示幅、初期値など
型定義構造体、列挙体など

このように役割の違う情報が1つのファイルに集まると、変更の影響範囲が大きくなりやすくなります。

たとえば、画面表示に関係する宣言を1行変更しただけなのに、計算処理を使っているファイルまで再コンパイルの対象になることがあります。

また、関数名やマクロ名が増えすぎると、名前の重複や意味の混乱も起こりやすくなります。

そのため、プログラムが大きくなってきたら、ヘッダーファイルも機能ごとに分けることを考えます。

関心ごとごとにヘッダーファイルを分ける

複数のヘッダーファイルに分けるときは、関心ごとで分けると整理しやすくなります。

関心ごととは、そのファイルが何を担当するのかという役割のことです。

たとえば、次のように分けることができます。

モジュール役割ヘッダーファイルソースファイル
計算処理計算用の関数をまとめるmathops.hmathops.c
画面表示表示用の関数をまとめるscreen.hscreen.c
設定管理定数や設定値をまとめるconfig.h必要に応じて作成

今回のサンプルでは、計算処理をmathops.hとmathops.cに分け、画面表示をscreen.hとscreen.cに分けます。

main.cは、それらの機能を利用する側です。

このように分けると、main.cは全体の流れを担当し、計算の詳細はmathops.c、表示の詳細はscreen.cに任せられます。

図:複数のヘッダーファイルは役割ごとに分ける

この図から分かること

この図から分かるのは、複数のヘッダーファイルを使うと、プログラムの役割を分けて管理できるということです。

main.cは必要なヘッダーファイルを読み込んで、計算処理や表示処理を利用します。計算関係はmathops.hとmathops.c、表示関係はscreen.hとscreen.cに分けることで、どのファイルが何を担当しているのか分かりやすくなります。

ヘッダーファイルとソースファイルはペアで考える

複数のヘッダーファイルに分けるときは、基本的にヘッダーファイルとソースファイルをペアで考えると理解しやすくなります。

たとえば、mathops.hには計算関数の宣言を書き、mathops.cには計算関数の本体を書きます。

screen.hには表示関数の宣言を書き、screen.cには表示関数の本体を書きます。

ヘッダーファイルソースファイル内容
mathops.hmathops.c計算関数の宣言と本体
screen.hscreen.c表示関数の宣言と本体

ヘッダーファイルには、外部に公開したい関数の宣言を書きます。
ソースファイルには、実際の処理内容を書きます。

この分け方にすると、呼び出し側のmain.cは、関数の詳しい中身を知らなくても、ヘッダーファイルを読み込むだけで関数を使えるようになります。

ヘッダーファイルには公開したい情報だけを書く

ヘッダーファイルには、他のファイルから使いたい情報を書きます。

具体的には、関数のプロトタイプ宣言、externによるグローバル変数の宣言、定数や型情報などです。

一方、関数の本体や、外部から見せる必要がない内部処理は.cファイルに書きます。

書く場所書く内容
.hファイル関数宣言、extern宣言、定数、型情報
.cファイル関数本体、グローバル変数の実体、内部処理

ヘッダーファイルに公開する情報を増やしすぎると、ファイル同士の依存関係が複雑になります。

そのため、ヘッダーファイルには必要な情報だけを書くことが大切です。

グローバル変数を共有するときはexternを使う

複数の.cファイルで同じグローバル変数を使いたい場合は、externを使います。

グローバル変数は、実体を1か所だけで定義する必要があります。

たとえば、計算結果を保存するcalcResというグローバル変数を使う場合、実体はmathops.cに1つだけ定義します。

int calcRes = 0;

ほかのファイルから参照したい場合は、ヘッダーファイルにextern宣言を書きます。

extern int calcRes;

externは、この変数の実体は別の場所にあります、という意味を持ちます。

これにより、複数のファイルから同じグローバル変数を参照できます。

externは宣言、実体の定義は1か所だけ

externを使うときに大切なのは、宣言と定義を区別することです。

書き方意味書く場所
int calcRes = 0;グローバル変数の実体を作るmathops.cなど1か所だけ
extern int calcRes;外部にある実体を参照する宣言mathops.hなど

もし複数の.cファイルにint calcRes = 0;を書いてしまうと、同じグローバル変数の実体が複数作られてしまいます。これは重複定義の原因になります。

一方、ヘッダーファイルにextern int calcRes;を書き、実体をmathops.cに1つだけ置けば、複数のファイルから安全に同じ変数を参照できます。

図:externでグローバル変数を共有する

この図から分かること

この図から分かるのは、グローバル変数を複数ファイルで共有するときは、実体の定義とextern宣言を分ける必要があるということです。

mathops.cにはcalcResの実体を1つだけ定義します。mathops.hにはextern宣言を書き、他のファイルはその宣言を通してcalcResを参照します。この形にすると、重複定義を防ぎながら共通の変数を使えます。

分割前のプログラムを確認する

まず、分割する前の形を確認してみましょう。

次のプログラムでは、計算処理、表示処理、グローバル変数が1つのファイルにまとまっています。

プロジェクト/ファイル名: Lesson45_1/no_split.c

#include <stdio.h>

int calcRes = 0; // 計算結果を保存するグローバル変数

void addScore(int, int);     // 加算する関数
void doubleScore(int);       // 2倍にする関数
void printRes(void);         // 結果を表示する関数

int main(void) {
    int base = 12;
    int bonus = 8;

    printf("%d + %d = ", base, bonus);
    addScore(base, bonus);
    printRes();

    printf("%d を2倍 = ", base);
    doubleScore(base);
    printRes();

    return 0;
}

// 2つの整数を加算してcalcResに保存する
void addScore(int a, int b) {
    calcRes = a + b;
}

// 整数を2倍してcalcResに保存する
void doubleScore(int n) {
    calcRes = n * 2;
}

// calcResの値を表示する
void printRes(void) {
    printf("%d\n", calcRes);
}

実行結果

12 + 8 = 20
12 を2倍 = 24

このプログラムは正しく動きますが、計算処理と表示処理が同じファイルに入っています。

小さなプログラムなら問題ありませんが、関数が増えてくるとmain関数周辺が長くなり、プログラム全体の見通しが悪くなります。

そこで、計算処理と表示処理を別々のヘッダーファイルとソースファイルに分けて整理します。

分割後のファイル構成

分割後は、次のような構成にします。

Lesson45_2/
 ├── main.c
 ├── mathops.h
 ├── mathops.c
 ├── screen.h
 └── screen.c

それぞれの役割は、次のとおりです。

ファイル名役割
main.cプログラム全体の流れを書く
mathops.h計算関数とcalcResのextern宣言を書く
mathops.c計算関数の本体とcalcResの実体を書く
screen.h表示関数の宣言を書く
screen.c表示関数の本体を書く

この構成では、計算処理と画面表示を別々に管理できます。

mathops.hを作成する

mathops.hには、計算関数のプロトタイプ宣言と、グローバル変数calcResのextern宣言を書きます。

プロジェクト/ファイル名: Lesson45_2/mathops.h

#ifndef MATHOPS_H
#define MATHOPS_H

// 計算結果を保存するグローバル変数のextern宣言
extern int calcRes;

// 2つの整数を加算する関数
void addScore(int, int);

// 整数を2倍する関数
void doubleScore(int);

#endif

このファイルでは、calcResの実体は作っていません。

extern int calcRes;

これは、calcResという変数の実体は別の.cファイルにあります、という宣言です。

また、addScore関数とdoubleScore関数のプロトタイプ宣言も書いています。

void addScore(int, int);
void doubleScore(int);

これにより、main.cやscreen.cなど、mathops.hを読み込んだファイルから、計算関数やcalcResの情報を参照できるようになります。

mathops.cを作成する

mathops.cには、calcResの実体と、計算関数の本体を書きます。

プロジェクト/ファイル名: Lesson45_2/mathops.c

#include "mathops.h"

int calcRes = 0; // 実体はこのファイルに1つだけ定義する

// 2つの整数を加算してcalcResに保存する
void addScore(int a, int b) {
    calcRes = a + b;
}

// 整数を2倍してcalcResに保存する
void doubleScore(int n) {
    calcRes = n * 2;
}

ここで大切なのは、calcResの実体をmathops.cに1つだけ定義していることです。

int calcRes = 0;

mathops.hに書いたextern int calcRes;は参照用の宣言でした。
実体はmathops.cにあるため、リンク時に同じ変数として結び付けられます。

screen.hを作成する

screen.hには、表示用関数のプロトタイプ宣言を書きます。

プロジェクト/ファイル名: Lesson45_2/screen.h

#ifndef SCREEN_H
#define SCREEN_H

// 計算結果を表示する関数
void printRes(void);

#endif

screen.hは、表示に関係する関数だけを公開します。

このように、画面表示用の宣言をscreen.hにまとめることで、計算用のmathops.hと役割を分けられます。

screen.cを作成する

screen.cには、printRes関数の本体を書きます。

printRes関数ではcalcResを表示するため、calcResのextern宣言が書かれているmathops.hを読み込みます。

また、printRes関数自身の宣言と本体を対応させるためにscreen.hも読み込みます。

プロジェクト/ファイル名: Lesson45_2/screen.c

#include <stdio.h>
#include "mathops.h"
#include "screen.h"

// calcResの値を表示する
void printRes(void) {
    printf("%d\n", calcRes);
}

screen.cでは、calcResを直接定義していません。

calcResの実体はmathops.cにあります。screen.cはmathops.hのextern宣言を通して、外部にあるcalcResを参照しています。

main.cを作成する

main.cでは、計算関数を使うためにmathops.hを読み込みます。
また、表示関数を使うためにscreen.hを読み込みます。

プロジェクト/ファイル名: Lesson45_2/main.c

#include <stdio.h>
#include "mathops.h"
#include "screen.h"

int main(void) {
    int base = 12;
    int bonus = 8;

    printf("%d + %d = ", base, bonus);
    addScore(base, bonus);
    printRes();

    printf("%d を2倍 = ", base);
    doubleScore(base);
    printRes();

    return 0;
}

実行結果

12 + 8 = 20
12 を2倍 = 24

main.cには、addScore関数やdoubleScore関数の本体はありません。
printRes関数の本体もありません。

それでも呼び出せるのは、main.cがmathops.hとscreen.hを読み込んでいるからです。

#include "mathops.h"
#include "screen.h"

ヘッダーファイルによって、main.cは関数の使い方を知ることができます。実際の処理はmathops.cやscreen.cにあり、ビルド時にリンクされます。

実行方法

1.「ビルド」 メニューから「ソリューションのビルド」を選択します。

2.ビルドが成功すると、「main.c」「mathops.c」「screen.c」の両方がコンパイルされ、正しくリンクされます。

3.「デバッグ」 メニューから「デバッグなしで開始」を選択します。

分割後の処理の流れ

分割後のプログラムでは、main.cが全体の流れを担当しています。

main.cからaddScore関数を呼び出すと、mathops.cにある関数本体が実行され、calcResに結果が保存されます。

その後、main.cからprintRes関数を呼び出すと、screen.cにある関数本体が実行され、calcResの値が表示されます。

流れを整理すると、次のようになります。

処理担当ファイル
全体の流れmain.c
加算や2倍の計算mathops.c
計算結果の保存mathops.cのcalcRes
結果の表示screen.c
関数や変数の宣言共有mathops.h、screen.h

複数のファイルに分けても、関数や変数の役割が整理されていれば、プログラム全体の流れは読みやすくなります。

図:複数ヘッダーと複数ソースはリンクで1つになる

この図から分かること

この図から分かるのは、複数のヘッダーファイルと複数のソースファイルが、ビルドの流れの中で1つの実行ファイルにまとまるということです。

ヘッダーファイルは宣言を共有し、.cファイルは関数本体や変数の実体を持ちます。externで宣言されたcalcResは、リンク時にmathops.cの実体と結び付けられます。

ビルド時に行われる処理

C言語のプログラムは、ソースコードを書いたらすぐに実行ファイルになるわけではありません。

一般的には、次のような段階を通って実行ファイルになります。

段階主な処理役割
プリプロセッサ#includeや#defineを展開するヘッダーの内容やマクロを処理する
コンパイルC言語のコードを変換するオブジェクトファイルを作る
アセンブルアセンブリを機械語へ変換するより低水準の形式にする
リンク複数のオブジェクトファイルを結合する実行ファイルを作る

Visual Studioでは、この一連の処理をビルドとして実行できます。

今回のLesson45_2では、main.c、mathops.c、screen.cがそれぞれコンパイルされます。その後、リンクによって1つの実行ファイルになります。

externはリンク時に解決される

extern int calcRes;は、calcResの実体が別の場所にあります、という宣言です。

コンパイルの段階では、screen.cはcalcResという変数が外部にあることを知っています。ただし、実体がどこにあるかはリンク時に結び付けられます。

今回の場合、calcResの実体はmathops.cにあります。

int calcRes = 0;

screen.cはmathops.hを通してextern宣言を見ています。

extern int calcRes;

リンカは、外部参照されているcalcResと、mathops.cにあるcalcResの実体を対応させます。

このしくみによって、複数の.cファイルから同じグローバル変数を参照できます。

複数ヘッダー分割で起こりやすいミス

複数のヘッダーファイルを使うときは、便利な反面、いくつかのミスに注意が必要です。

ヘッダーファイルに実体を書いてしまう

ヘッダーファイルに次のように書くと、複数の.cファイルで読み込まれたときに重複定義の原因になります。

int calcRes = 0;

グローバル変数の実体は、.cファイルの1か所だけに書きます。

ヘッダーファイルには、extern宣言を書きます。

extern int calcRes;

インクルードガードを書き忘れる

ヘッダーファイルには、インクルードガードを書いておくと安全です。

#ifndef SCREEN_H
#define SCREEN_H

void printRes(void);

#endif

これにより、同じヘッダーファイルが複数回読み込まれても、中身が重複して展開されるのを防げます。

必要以上に#includeを書きすぎる

必要のないヘッダーファイルまで読み込むと、依存関係が増えます。

依存関係が増えると、1つのヘッダーファイルを変更したときに、再コンパイルされる範囲が広がりやすくなります。

必要なファイルだけを読み込むことを意識しましょう。

宣言と定義が一致していない

ヘッダーファイルの宣言と、ソースファイルの関数定義は一致している必要があります。

void addScore(int, int);

void addScore(int a, int b) {
    calcRes = a + b;
}

戻り値の型、関数名、引数の数、引数の型が合っているか確認しましょう。

ヘッダー分割で意識したいベストプラクティス

複数のヘッダーファイルを使うときは、次の点を意識すると整理しやすくなります。

ポイント内容
役割ごとに分ける計算、表示、設定などで分ける
ヘッダーには宣言を書く関数本体や変数の実体は.cファイルに置く
グローバル変数はexternで共有する実体は1か所だけにする
インクルードガードを書く多重読み込みを防ぐ
必要なヘッダーだけ読み込む依存関係を増やしすぎない
公開する情報を絞る外部から使うものだけヘッダーに書く

特に大切なのは、ヘッダーファイルを公開窓口として考えることです。

外部から使ってよい関数や変数の宣言だけをヘッダーファイルに書き、細かい処理内容は.cファイルに隠しておくと、プログラムの構造が分かりやすくなります。

変更範囲を小さくすると保守しやすい

複数ヘッダーに分けるメリットの1つは、変更範囲を小さくできることです。

たとえば、画面表示の形式だけを変えたい場合は、screen.cを中心に修正します。計算方法を変えたい場合は、mathops.cを中心に修正します。

変更したい内容主に確認するファイル
計算方法を変えるmathops.c
計算関数の名前や引数を変えるmathops.h、mathops.c、利用側
表示形式を変えるscreen.c
表示関数を追加するscreen.h、screen.c
mainの処理順を変えるmain.c

ヘッダーファイルを変更すると、そのヘッダーを読み込んでいるファイルにも影響します。そのため、公開インターフェースである.hファイルは、できるだけ慎重に変更することが大切です。

一方、関数の中身だけを変えるなら、.cファイル側の変更で済むことが多くなります。

複数のヘッダーファイルで大きなプログラムを整理する

複数のヘッダーファイルへの分割は、C言語のプログラムをより実践的に整理するための大切な考え方です。

1つのヘッダーファイルにすべてを詰め込むのではなく、計算処理はmathops.h、表示処理はscreen.hのように、関心ごとごとに分けると見通しがよくなります。

グローバル変数を共有したい場合は、extern宣言をヘッダーファイルに書き、実体の定義は.cファイルの1か所だけにします。これにより、重複定義を防ぎながら、複数のファイルから同じ変数を参照できます。

また、ヘッダーファイルにはインクルードガードを書き、多重読み込みを防ぎます。必要なヘッダーファイルだけを読み込むようにすると、依存関係が整理され、ビルドや保守もしやすくなります。

main.c、mathops.c、screen.cのように複数のソースファイルへ分けても、ビルド時にはコンパイルとリンクによって1つの実行ファイルとしてまとまります。この流れを理解すると、ファイルを分けてもプログラムが1つとして動く理由が見えてきます。

複数ヘッダーの分割に慣れると、C言語のプログラムを機能ごとに整理できるようになります。小さな学習用プログラムからでも、関数の役割やファイルの責任範囲を意識して書くことで、保守しやすいコードへ一歩近づけます。