6日でできる 新C言語入門|ソースコード分割の基本ルール

共有するものはヘッダーへ、処理の中身はソースへ。
ソースコード分割の基本ルールを身につけると、C言語プログラムは大きくなっても読みやすく保ちやすくなります。

C言語でプログラムを書き始めたばかりのころは、main.cだけにすべての処理を書いても問題なく学習できます。小さなプログラムであれば、1つのファイルに関数や変数をまとめても、全体を見渡しやすいからです。

しかし、プログラムの規模が大きくなると、1つのファイルにすべてを書く方法ではだんだん管理が難しくなります。関数が増え、構造体が増え、定数が増え、処理の種類も増えてくると、どこに何が書かれているのか分かりにくくなります。

そこで大切になるのが、ソースコード分割です。

C言語では、主にヘッダーファイルとソースファイルを使って、プログラムを役割ごとに分けます。ヘッダーファイルには、他のファイルから使うための宣言を書きます。ソースファイルには、実際の処理内容を書きます。

この分け方をきちんと理解しておくと、プログラムの見通しが良くなり、修正もしやすくなります。また、同じ機能を別のプログラムでも再利用しやすくなります。

この記事では、ソースコード分割の基本ルールとして、ヘッダーファイルに書くもの、ソースファイルに書くもの、staticで隠すもの、externで共有するもの、インクルードガードの役割を整理しながら、Lesson70_1のサンプルプログラムで確認していきます。

ソースコード分割は役割を分けるために行う

ソースコード分割の目的は、単にファイル数を増やすことではありません。

大切なのは、役割ごとにファイルを分けることです。

たとえば、書籍データを扱うプログラムなら、書籍データの型や関数の宣言をbook.hに書き、実際の処理をbook.cに書き、main.cではプログラム全体の流れを記述します。

ファイル主な役割
book.h他のファイルから使う型、定数、関数宣言をまとめる
book.c書籍データを作成・表示する処理の中身を書く
main.cプログラム全体の流れを書く

このように分けると、main.cは全体の流れに集中できます。
book.cは書籍データの処理に集中できます。
book.hは、外部に見せる入口として使えます。

ヘッダーファイルとソースファイルの基本的な考え方

C言語の分割では、ヘッダーファイルとソースファイルの役割を分けて考えます。

種類拡張子主な役割
ヘッダーファイル.h他のファイルから使うための宣言を書く
ソースファイル.c関数の本体や内部処理を書く

ヘッダーファイルは、別のファイルへ公開するための窓口です。

ソースファイルは、実際に処理を行う場所です。

たとえば、book.hにprintBookのプロトタイプ宣言を書いておけば、main.cからprintBookを呼び出せます。実際のprintBookの処理内容はbook.cに書きます。

このように、宣言と実装を分けることがソースコード分割の基本です。

図:ヘッダーファイルとソースファイルの役割

この図から分かること

この図から分かるのは、ヘッダーファイルとソースファイルには役割の違いがあるということです。

book.hには、他のファイルから使うための構造体、定数、関数プロトタイプを書きます。book.cには、関数の中身や内部で使う変数を書きます。main.cはbook.hを読み込むことで、book.cにある関数を利用できるようになります。

ヘッダーファイルに書くもの

ヘッダーファイルには、他のファイルから使う必要がある情報を書きます。

代表的な内容は次の通りです。

書く内容役割
必要なヘッダーのインクルード型や関数を使うために必要なヘッダーを読み込む
マクロ定義外部と共有したい定数を定義する
列挙型の定義外部で使う状態や種類を名前付きで定義する
構造体の定義複数ファイルで共有するデータ型を定義する
関数プロトタイプ宣言他のファイルから呼び出せる関数を宣言する
extern宣言他のファイルにある変数を参照するために宣言する

ただし、何でもヘッダーファイルに書けばよいわけではありません。

ヘッダーファイルに書くのは、外部から使う必要があるものだけです。
そのファイルの中だけで使う変数や補助関数は、ヘッダーファイルには書かず、ソースファイルの中に置きます。

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

ヘッダーファイルは、他のファイルに見せるための場所です。

たとえば、書籍データを扱う機能を外部から使えるようにするなら、Book構造体、MAX_TITLE、createBook、printBookなどをbook.hに書きます。

一方で、book.cの中だけで使うカウンタ変数や補助関数は、book.hに書く必要はありません。

ヘッダーファイルに書くヘッダーファイルに書かない
外部から使う構造体ファイル内だけで使うstatic変数
外部から使う定数内部処理だけで使う補助関数
外部から呼ぶ関数の宣言関数の具体的な処理内容
他ファイルと共有するenum内部専用の定数や状態

公開範囲を必要最小限にすることで、プログラム全体の依存関係を整理しやすくなります。

book.hの例

次のヘッダーファイルでは、書籍データを表す構造体、タイトルの最大長、関数プロトタイプを定義しています。

プロジェクト/ファイル名: Lesson70_1/book.h

#ifndef _BOOK_H_
#define _BOOK_H_

#include <stdio.h>

#define MAX_TITLE 100
#define MAX_AUTHOR 60

typedef struct {
    int id;
    char title[MAX_TITLE];
    char author[MAX_AUTHOR];
} Book;

Book createBook(int, const char*, const char*);
void printBook(const Book*);
int getBookCount(void);

#endif

このbook.hでは、最初と最後にインクルードガードを書いています。

#ifndef _BOOK_H_
#define _BOOK_H_

#endif

インクルードガードは、同じヘッダーファイルが複数回読み込まれても、重複して定義されないようにするための仕組みです。

Book構造体には、id、title、authorの3つのメンバがあります。

メンバ内容
id書籍ID
title書籍タイトル
author著者名

createBookは、Book型のデータを作成する関数です。
printBookは、Book型のデータを表示する関数です。
getBookCountは、作成された書籍データの件数を確認する関数です。

インクルードガードの役割

ヘッダーファイルには、インクルードガードを書きます。

#ifndef _BOOK_H_
#define _BOOK_H_

/* ヘッダーファイルの内容 */

#endif

インクルードガードは、同じヘッダーファイルの内容が何度も読み込まれることを防ぎます。

複数ファイル構成では、main.cがbook.hを読み込み、book.cもbook.hを読み込むような形になります。さらに、別のヘッダーからbook.hが読み込まれることもあります。

もしインクルードガードがなければ、同じ構造体や関数宣言が重複して読み込まれ、コンパイルエラーの原因になることがあります。

役割
#ifndef BOOK_Hまだ_BOOK_H_が定義されていなければ、次の内容を読み込む
#define BOOK_H_BOOK_H_を定義し、次回以降の重複読み込みを防ぐ
#endifインクルードガードの範囲を閉じる

ヘッダーファイルを作るときは、インクルードガードを必ず書く習慣をつけておくと安心です。

図:インクルードガードは二重読み込みを防ぐ

この図から分かること

この図から分かるのは、複数のファイルから同じヘッダーファイルが読み込まれることがあるということです。

インクルードガードがあると、book.hの内容が最初の1回だけ有効になり、2回目以降の重複定義を防げます。構造体やマクロを定義するヘッダーファイルでは、とても重要な仕組みです。

ソースファイルに書くもの

ソースファイルには、実際の処理内容を書きます。

ヘッダーファイルが宣言の場所なら、ソースファイルは実装の場所です。

書く内容役割
対応するヘッダーのインクルード自分が実装する関数の宣言や型を確認する
必要な標準ヘッダーのインクルードstring.hなど、処理に必要なヘッダーを読み込む
内部専用のマクロ定義その.cファイル内だけで使う定数を定義する
内部専用のstatic変数外部から触らせたくない変数を定義する
内部専用のstatic関数ファイル内だけで使う補助関数を定義する
関数の定義ヘッダーで宣言した関数の本体を書く

ソースファイルでは、外部から見せる必要がないものをstaticで隠すのが重要です。

staticで内部専用の変数を隠す

ファイルの外から直接使わせたくない変数には、staticを付けます。

static int book_counter = 0;

このように書くと、book_counterはbook.cの中だけで使える変数になります。
main.cから直接book_counterを変更することはできません。

書き方意味
int book_counter外部から参照される可能性があるグローバル変数
static int book_counterこの.cファイル内だけで使えるグローバル変数

内部状態を外部から直接変更できるようにすると、プログラムの整合性が崩れることがあります。

たとえば、作成された書籍数を数えるbook_counterをmain.cから自由に書き換えられると、実際の処理結果と件数が一致しなくなるかもしれません。

staticで隠しておけば、件数の更新はbook.c内の関数に任せられます。

book.cの例

次のソースファイルでは、book.hで宣言した関数の中身を実装しています。

プロジェクト/ファイル名: Lesson70_1/book.c

#include "book.h"
#include <string.h>

static int book_counter = 0; // ファイル内限定のグローバル変数

static void clearBook(Book* b); // ファイル内だけで使う補助関数

Book createBook(int id, const char* title, const char* author) {
    Book b;

    // 構造体の内容を初期化する
    clearBook(&b);

    // 書籍データを設定する
    b.id = id;

    strncpy(b.title, title, MAX_TITLE - 1);
    b.title[MAX_TITLE - 1] = '\0';

    strncpy(b.author, author, MAX_AUTHOR - 1);
    b.author[MAX_AUTHOR - 1] = '\0';

    // 作成件数を増やす
    book_counter++;

    return b;
}

void printBook(const Book* b) {
    if (b != NULL) {
        printf("書籍ID: %d, タイトル: %s, 著者: %s\n",
            b->id, b->title, b->author);
    }
}

int getBookCount(void) {
    return book_counter;
}

static void clearBook(Book* b) {
    if (b != NULL) {
        b->id = 0;
        strcpy(b->title, "");
        strcpy(b->author, "");
    }
}

このbook.cでは、最初にbook.hを読み込んでいます。

#include "book.h"

対応するヘッダーファイルを読み込むことで、Book構造体やMAX_TITLE、MAX_AUTHOR、関数プロトタイプを使えるようになります。

また、文字列コピーでstrncpyやstrcpyを使うため、string.hも読み込んでいます。

#include <string.h>

対応するヘッダーをソースファイルで読み込む理由

book.cでは、book.hを読み込んでいます。

これは、book.cがbook.hで公開している関数の本体を書くファイルだからです。

対応するヘッダーを読み込むことで、関数プロトタイプと関数定義の食い違いに気づきやすくなります。

たとえば、book.hではcreateBookが次のように宣言されています。

Book createBook(int, const char*, const char*);

book.cでも同じ形で定義します。

Book createBook(int id, const char* title, const char* author)

もし引数の数や型がずれていた場合、コンパイラが警告やエラーとして教えてくれることがあります。

そのため、ソースファイルでは対応するヘッダーファイルを読み込むのが基本です。

ファイル内だけで使う補助関数はstaticにする

book.cでは、clearBookという補助関数を用意しています。

static void clearBook(Book* b);

この関数は、Book構造体の内容を初期化するためにbook.cの中だけで使います。
main.cから直接呼び出す必要はありません。

そのため、staticを付けています。

static void clearBook(Book* b)

関数にstaticを付けると、その関数は同じ.cファイル内だけで使える関数になります。

関数公開するか理由
createBook公開するmain.cから呼び出す
printBook公開するmain.cから呼び出す
getBookCount公開するmain.cから呼び出す
clearBook公開しないbook.c内部だけで使う補助処理

このように、外部から呼び出す必要がない関数はstaticで隠すと、ファイルの役割が明確になります。

main.cで分割した機能を使う

main.cでは、book.hを読み込んで、Book構造体や関数を利用します。

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

#include <stdio.h>
#include "book.h"

int main(void) {
    Book b1 = createBook(10, "C言語設計入門", "青木太郎");
    Book b2 = createBook(20, "やさしい分割コンパイル", "佐藤花子");

    // 作成した書籍データを表示する
    printBook(&b1);
    printBook(&b2);

    // book.c内で管理している作成件数を確認する
    printf("作成した書籍数: %d\n", getBookCount());

    return 0;
}

実行結果

書籍ID: 10, タイトル: C言語設計入門, 著者: 青木太郎
書籍ID: 20, タイトル: やさしい分割コンパイル, 著者: 佐藤花子
作成した書籍数: 2

main.cでは、book.hを読み込んでいます。

#include "book.h"

これにより、Book型、createBook、printBook、getBookCountを使えるようになります。

main.cは、book_counterやclearBookを直接使っていません。
book_counterはbook.cの内部状態であり、clearBookはbook.cの内部専用関数だからです。

このように、外部から使う必要がある機能だけをヘッダーで公開し、内部処理はソースファイルに隠すことで、分割設計がきれいになります。

図:公開するものと隠すものを分ける

この図から分かること

この図から分かるのは、分割設計では公開するものと隠すものを分けることが大切だということです。

main.cはbook.hを通して、Book構造体や公開関数を使います。一方、book.cのbook_counterやclearBookはstaticで隠されているため、外部から直接使えません。このようにすると、内部状態を守りながら、必要な機能だけを安全に公開できます。

ヘッダーに書きすぎないことが大切

ヘッダーファイルに何でも書いてしまうと、他のファイルとの依存関係が強くなります。

たとえば、内部専用の変数や補助関数までヘッダーに書いてしまうと、本来使わせたくないものまで外部から見えてしまいます。

ヘッダーに書く内容は、次の基準で考えると整理しやすくなります。

判断基準ヘッダーに書くか
他のファイルから使う型書く
他のファイルから呼ぶ関数書く
複数ファイルで共有する定数書く
その.cファイルだけで使う変数書かない
その.cファイルだけで使う補助関数書かない
関数の具体的な処理内容書かない

ヘッダーファイルは、外部に公開する約束だけを書く場所と考えると分かりやすいです。

ソースファイルには処理の中身を書く

ソースファイルには、関数の具体的な処理を書きます。

たとえば、createBookはbook.hで宣言し、book.cで定義しています。

ファイル内容
book.hBook createBook(int, const char*, const char*);
book.ccreateBookの実際の処理

このように分けることで、main.cはcreateBookの使い方だけを知っていればよくなります。

内部でstrncpyを使っていることや、book_counterを増やしていることまでは、main.cが知る必要はありません。

これは、プログラムの部品化に近い考え方です。
使う側は使い方を知り、作る側は中身を管理します。

staticとexternの使い分け

分割設計では、staticとexternも重要です。

指定子役割
staticファイル内だけで使えるようにする
extern別ファイルにある変数を参照する

今回のbook_counterは、book.cの中だけで管理したい変数なのでstaticにしています。

static int book_counter = 0;

これにより、main.cからbook_counterを直接書き換えられません。

一方、複数ファイルで共有したいグローバル変数がある場合は、externを使って参照できます。

ただし、externで共有する変数は、使いすぎに注意が必要です。
どのファイルからでも変更できる変数が増えると、値の変化を追いにくくなります。

基本的には、内部状態はstaticで隠し、必要な操作は関数として公開するほうが安全です。

同じ定義を複数の場所に書かない

分割設計で注意したいことの1つが、同じ定義を複数の場所に書かないことです。

たとえば、Book構造体の定義をbook.hにもbook.cにも別々に書くと、内容がずれたときに問題になります。

構造体やマクロのように複数ファイルで共有するものは、ヘッダーファイルに1回だけ書きます。

よくない例理由
同じ構造体定義を複数ファイルに書く修正漏れや不一致の原因になる
同じマクロを複数ファイルで別々に定義する値がずれる可能性がある
関数プロトタイプを各.cに手書きする宣言と定義が食い違いやすい

共有したい定義はヘッダーファイルへまとめ、必要なファイルでincludeするのが基本です。

インクルードする順番も意識する

ソースファイルでは、インクルードする順番をある程度そろえておくと読みやすくなります。

よく使われる並びは次のような形です。

順番内容
1対応する自作ヘッダー
2必要な標準ヘッダー
3その他の自作ヘッダー

book.cでは、まず対応するbook.hを読み込み、そのあとstring.hを読み込んでいます。

#include "book.h"
#include <string.h>

main.cでは、標準入出力を使うためstdio.hを読み込み、さらにbook.hを読み込んでいます。

#include <stdio.h>
#include "book.h"

インクルードの順番をそろえておくと、依存関係が見やすくなります。

循環参照に注意する

ヘッダーファイル同士が互いにincludeし合う状態を循環参照と呼びます。

たとえば、a.hがb.hを読み込み、b.hがa.hを読み込むような状態です。

循環参照があると、定義の順番が複雑になり、コンパイルエラーや読みづらさの原因になります。

循環参照を避けるためには、次のような考え方が役立ちます。

対策内容
不要なincludeを減らす本当に必要なヘッダーだけ読み込む
前方宣言を使う型の詳細が不要なら宣言だけで済ませる
役割を分けるデータ定義と処理宣言を整理する
共通ヘッダーを作る共通の型だけを別ヘッダーにまとめる

学習段階では、まず不要なincludeを書かないことを意識するとよいです。

分割コンパイルとリンクの流れ

複数の.cファイルがある場合、それぞれのソースファイルはコンパイルされ、最後にリンクされて1つの実行ファイルになります。

今回の例では、main.cとbook.cがそれぞれコンパイルされ、リンクされます。

段階内容
コンパイルmain.cやbook.cをそれぞれ機械語に近い形へ変換する
リンク各ファイルの関数呼び出しを結び付けて実行ファイルを作る

main.cではcreateBookを呼び出していますが、その本体はbook.cにあります。

コンパイル時には、book.hのプロトタイプ宣言によって、createBookという関数があることを知ります。
リンク時には、book.cにあるcreateBookの実体と結び付けられます。

そのため、Visual Studioなどの開発環境では、main.cだけでなくbook.cもプロジェクトに追加してビルドする必要があります。

Visual Studioでのビルド

1.「SDLチェック」を「いいえ」にします。
2.メニューの「ビルド」から「ソリューションのビルド」を実行します。

Visual Studioでビルドするときのポイント

複数ファイル構成では、必要な.cファイルがプロジェクトに含まれているかを確認しましょう。

今回のLesson70_1では、少なくとも次のファイルが必要です。

ファイル必要な理由
main.cmain関数がある
book.ccreateBook、printBook、getBookCountの本体がある
book.hBook型や関数プロトタイプを共有する

book.hはヘッダーファイルなので、関数の宣言はありますが、関数の本体はありません。

もしbook.cがプロジェクトに含まれていないと、createBookやprintBookの本体が見つからず、リンクエラーになります。

「未解決の外部シンボル」のようなエラーが出た場合は、関数の本体がある.cファイルがビルド対象に含まれているかを確認するとよいです。

ソースコード分割でよくあるミス

ソースコード分割では、次のようなミスがよくあります。

ミス内容
ヘッダーに関数の本体を書いてしまう複数ファイルから読み込むと重複定義の原因になる
インクルードガードを書かない同じ定義が複数回読み込まれる可能性がある
.cファイルをプロジェクトに追加し忘れるリンクエラーの原因になる
内部変数をstaticにしない外部から不要に見えてしまう
ヘッダーに内部専用の宣言を書く使わせたくない機能まで公開してしまう
同じ構造体を複数ファイルに別々に書く定義の不一致が起きやすい

特に、ヘッダーに何を書くか、ソースに何を書くかを意識することが重要です。

迷ったときは、他のファイルから使う必要があるかを基準に考えましょう。

分割設計の基本ルールを整理する

ソースコード分割では、次のルールを意識すると整理しやすくなります。

ルール内容
共有するものはヘッダーへ構造体、定数、関数プロトタイプなどを書く
実装はソースへ関数の中身や処理ロジックを書く
内部専用はstaticで隠す外部から使わない変数や関数を隠す
同じ定義を重複させない共有定義はヘッダーにまとめる
インクルードガードを書く二重読み込みを防ぐ
.cファイルをビルド対象に入れる関数本体をリンクできるようにする
main.cは流れに集中する詳細な処理は別ファイルへ分ける

このルールを守ると、ファイルごとの役割がはっきりし、プログラムが大きくなっても管理しやすくなります。

ソースコード分割を身につけると設計力が上がる

ソースコード分割は、C言語で大きめのプログラムを書くための基本技術です。

ヘッダーファイルには、他のファイルから使う型、定数、関数プロトタイプを書きます。
ソースファイルには、実際の関数定義や内部処理を書きます。
外部から直接使わせたくない変数や補助関数にはstaticを付けて隠します。

Lesson70_1では、book.h、book.c、main.cに分けて、書籍データを作成して表示するプログラムを確認しました。

book.hは公開する情報をまとめる場所です。
book.cは処理の中身を実装する場所です。
main.cは全体の流れを書く場所です。

この分け方を理解すると、C言語プログラムを部品の集まりとして設計できるようになります。
プログラムが大きくなっても、役割ごとに整理されていれば、読みやすく、直しやすく、再利用しやすいコードになります。