C言語 ファイル分割の考え方【何を基準に分けるのかを解説】

C言語
この記事は約18分で読めます。

こんにちは、ナナです。

ここまでは主に1つのソースファイルを使って、C言語の機能を学んできました。

ただ、これはあくまでも学習用であり、実践的なソフトウェアシステムではたくさんのファイルを扱うことになります。

ファイルをいくつも作るって、どうして分けるの?1つじゃダメなの?分けるにしても、何をどうやって分ければいいの?

慣れないうちは、どのようにファイルを分けるべきかの判断がなかなかできないんですね。

そんなファイル構成をどうすればよいのかわからない方に、ファイルをどのような視点で分割するかを解説していきます。

本記事では次の疑問点を解消する内容となっています。

本記事で学習できること
  • ファイル分割をするための考え方とは?
  • 「機能分割」と「ファイル分割」の関係性とは?
  • ソースファイルとヘッダファイルの書き方とは?

C言語におけるシステム構築のためのファイル分割方法について学びます。

スポンサー

「ファイル分割」とは「機能分割」すること

押忍!自分はいつも「main.c」のファイルにプログラムを書いてるっす。このままずっとmain.cに書き続ければいいっすよね?

1,000行でも2,000行でもここに書き続ければ、その先にゴールがあると信じてるっす!

ナナ
ナナ

うん、それは良くないね。1つのファイルにプログラムをずっと書き続けたら、管理しづらいよね。

じゃあ、ファイル分割の考え方を学ぼうね。このまま突っ走っちゃうと大変なことになっちゃうからね。

皆さん、家に本があったときに「雑誌」「漫画」「小説」とか分類分けをして棚に整理していませんか?

分類を分けて管理するという考え方は、プログラムの世界でも有効なんです。

分類分けで整理整頓

分類分けをするメリットは、目的のものが探しやすくなり、管理もしやすくなることです。

システム構成の検討と機能分割

皆さんは何のためにC言語を学ぶのでしょう。それは何かを作りたいからですよね。

我々開発者にとって作りたいものとは『ソフトウェアシステム』であり、それを表現する手段がプログラミング言語です。

ここで気をつけるべきことがあります。

開発経験のない方がソフトウェアシステムを作ろうとすると、設計もせずに全てを一度に作ろうとしてしまいがちですが、これはやってはなりません。

欠陥システム

それは全体の構成も考えずに、ビルを建てようとするのと一緒です。

プログラムでシステム開発を行うには言語的なスキルだけでなく、システム全体をどのように構築していくかを検討するスキルも必要となります。

ここで出てくるのが「ファイル分割」です。

ファイル分割とは、

如何にシステムを作り上げるかという重要な設計技術

なのです。

「機能分割」と「ファイル分割」の関係性

皆さんが作りたいシステムが、どのような構成となっているかをじっくり検討する時間を作りましょう。

まずは、作りたい対象のシステムにおいてどのような機能があるかを洗い出し、機能と機能の関係性を考えることです。

機能分割とファイル分割

機能の抽出ができたら機能毎にファイルを配置します。

今の段階ではあまりピンとこないかもしれません。小さなシステム開発から経験を積んでいくことで徐々に感覚が身についていきます。

ナナ
ナナ

今は「機能」に着目して「ファイルを分割する」という考え方を覚えてください。実践的にはマイコン入門編のカリキュラムで体験していただきます。

C言語で管理するファイルの種類

C言語のプログラムはソースファイルとヘッダファイルで構成されるのでした。

C言語の構成ファイル
ソースとヘッダファイル

実際のシステム開発では、2つのファイルだけでシステムを構築というわけにはいきません。

「ソースファイル」と「ヘッダファイル」を対にして、複数のセットを用意するのが最も一般的なシステム構成です。

全体のファイル構成
機能単位でファイルを準備

分割した機能に対して、ソースファイルとヘッダファイルを配置しシステムを構築していきます。

ナナ
ナナ

C言語のソフトウェアシステムは、「ソースファイル」と「ヘッダファイル」の集合体で表現されます。この構成を如何に上手に構築するかが開発者の腕の見せ所です!

スポンサー

ファイル分割するための「ソースファイル」と「ヘッダファイル」の書き方

押忍っ!ファイルはmain.cだけじゃダメなんすね。

ソースファイルとヘッダファイルって何が違うんすか?どうやって書いたらいいのか、さっぱりわかんないっすよ!

ナナ
ナナ

そうだね。ここまで学習してきた「変数」や「関数」や「構造体」といった部品は、適切にソースファイルとヘッダファイルに分けて書いていく必要があるね。

C言語をここまで学んできた中で様々な部品が登場しました。

例えば、皆さんが「構造体」という部品を定義したい、と思った時にはいったいどのファイルのどこに書くのが正解なのでしょうか。

C言語の構成部品

C言語のプログラムを構成する部品は、大きく分けて次の項目で構成されます。

部品役割
ヘッダファイルインクルード#includeによる外部ファイルのインクルード
マクロ定義#defineによる定数定義
型定義構造体、列挙型、共用体のデータ型定義
プロトタイプ宣言関数のプロトタイプ宣言
グローバル変数外部参照宣言外部ファイルへ公開するグローバル変数の参照宣言
グローバル変数定義グローバル変数の実体定義
関数定義関数の実体定義

これらの部品を、ソースファイルとヘッダファイルに振り分けて書いていくことになります。

ナナ
ナナ

各部品の配置場所は理にかなったルールに従って配置することになります。詳細は続きをご覧ください。

ソースファイルとヘッダファイルの書き方講座

各ファイルの書き方は、別の記事にまとめてあります。それぞれの書き方講座を参照ください。

スポンサー

課題:ファイル分割の考え方が学べたかを確認しよう

課題1

課題内容

プロジェクトにmain.c、sub.c、sub.hの3ファイルを登録せよ。

次の関数をsub.cへ定義せよ。

課題1_1

main.cから上記関数を呼び出せるようにsub.hに必要な情報を記載しインクルード処理を追加せよ。プログラムを実行し出力期待結果が表示されることを確認せよ。

main.c

#include <stdio.h>

int main(void)
{
    sub_Hello();
    return 0;
}

出力期待結果

Hello

main.c

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

int main(void)
{
    sub_Hello();
    return 0;
}

sub_Hello関数を呼び出すためにsub.hをインクルードする必要がある。


sub.h

#ifndef SUB_H
#define SUB_H
//------------------------------------------------

//------------------------------------------------
//  プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
void sub_Hello(void);

//------------------------------------------------
#endif

main.cからsub_Hello関数を呼び出し可能とするためプロトタイプ宣言を行う。ヘッダファイルのため多重インクルード防止を施すことを忘れないこと。


sub.c

#include <stdio.h>

void sub_Hello(void)
{
    printf("Hello");
}

課題2

課題内容

プロジェクトにmain.c、fruit.c、fruit.hの3ファイルを登録し、各ファイル内容を次のプログラムとせよ。

main.c

#include <stdio.h>

//------------------------------------------------
//  概 要:メイン処理
//  引  数:なし
//  戻り値:0 正常
//------------------------------------------------
int main(void)
{
    fruit_printFruitInfo();

    main_calcFruit();

    return 0;
}

fruit.c

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

//------------------------------------------------
//  概 要:全フルーツ情報の出力
//  引  数:なし
//  戻り値:なし
//------------------------------------------------
void fruit_printFruitInfo(void)
{
    E_FRUIT_KIND_ID i;

    printf("--------------------------------------------\n");

    for (i = 0; i < E_FRUIT_ID_END; i++)
    {
        printf("名前:%-16s\t価格:%5d円\n",
            gFruitTbl[i].name, gFruitTbl[i].price);
    }

    printf("--------------------------------------------\n");
}

fruit.h

#ifndef FRUIT_H
#define FRUIT_H
//------------------------------------------------

//------------------------------------------------
//  マクロ定義(Macro definition)
//------------------------------------------------


//------------------------------------------------
//  型定義(Type definition)
//------------------------------------------------


//------------------------------------------------
//  プロトタイプ宣言(Prototype declaration)
//------------------------------------------------


//------------------------------------------------
#endif

次の部品を3ファイルに適切に配置し、ビルドと実行ができるようにせよ。

各部品は必ず1回だけ配置することとする。定義の参照範囲はより狭くなるように部品を配置すること。適切に配置できれば出力期待結果が表示される。

部品一覧

//------------------------------------------------
//  フルーツテーブル定義
//------------------------------------------------
const static S_FRUIT_INFO gFruitTbl[] =
{
    {   "みかん",       50},
    {   "桃",           400},
    {   "バナナ",       100},
    {   "パイナップル", 250},
};
typedef enum
{
    E_FRUIT_ID_ORANGE   = 0,
    E_FRUIT_ID_PEACH,
    E_FRUIT_ID_BANANA,
    E_FRUIT_ID_PINE,
    //------------------------
    E_FRUIT_ID_END,   //  番兵
} E_FRUIT_KIND_ID;
//-------------------------------------------------
//  概 要:フルーツ購入価格の算出表示
//  引  数:なし
//  戻り値:なし
//-------------------------------------------------
static void main_calcFruit(void)
{
    long allPrice = 0;

    allPrice += fruit_getPriceFruit(E_FRUIT_ID_PEACH, 3);   //  桃3個
    allPrice += fruit_getPriceFruit(E_FRUIT_ID_BANANA, 5);  //  バナナ5本

    printf("購入価格:%d", allPrice);
}
static void main_calcFruit(void);
typedef struct
{
    char name[D_FRUIT_NAMESIZE]; //  名前
    long price;                  //  価格
} S_FRUIT_INFO;
//------------------------------------------------
//  概 要:フルーツ代金の取得。
//          フルーツIDと個数から代金を算出する
//  引  数:id  算出するフルーツID
//          num 個数
//  戻り値:0以上   算出した代金
//          負値    異常時
//------------------------------------------------
long fruit_getPriceFruit(E_FRUIT_KIND_ID id, unsigned short num)
{
    if (id < 0 || id >= E_FRUIT_ID_END)
    {
        return -1;
    }

    return gFruitTbl[id].price * num;
}
void fruit_printFruitInfo(void);
long fruit_getPriceFruit(E_FRUIT_KIND_ID id, unsigned short num);
#include "fruit.h"
#define D_FRUIT_NAMESIZE   (32)

出力期待結果

--------------------------------------------
名前:みかん            価格:   50円
名前:桃                価格:  400円
名前:バナナ            価格:  100円
名前:パイナップル      価格:  250円
--------------------------------------------
購入価格:1700

main.c

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

static void main_calcFruit(void);

//------------------------------------------------
//  概 要:メイン処理
//  引  数:なし
//  戻り値:0 正常
//------------------------------------------------
int main(void)
{
    fruit_printFruitInfo();

    main_calcFruit();

    return 0;
}

//-------------------------------------------------
//  概 要:フルーツ購入価格の算出表示
//  引  数:なし
//  戻り値:なし
//-------------------------------------------------
static void main_calcFruit(void)
{
    long allPrice = 0;

    allPrice += fruit_getPriceFruit(E_FRUIT_ID_PEACH, 3);   //  桃3個
    allPrice += fruit_getPriceFruit(E_FRUIT_ID_BANANA, 5);  //  バナナ5本

    printf("購入価格:%d", allPrice);
}

mainで始まるmain_calcFruit関数はmain.cに配置する。static関数のためプロトタイプ宣言も追加する。また、フルーツ情報関連の関数を呼び出すためfruit.hをインクルードする。


fruit.c

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

#define D_FRUIT_NAMESIZE   (32)

typedef struct
{
    char name[D_FRUIT_NAMESIZE]; //  名前
    long price;                  //  価格
} S_FRUIT_INFO;

//------------------------------------------------
//  フルーツテーブル定義
//------------------------------------------------
const static S_FRUIT_INFO gFruitTbl[] =
{
    {   "みかん",       50},
    {   "桃",           400},
    {   "バナナ",       100},
    {   "パイナップル", 250},
};

//------------------------------------------------
//  概 要:全フルーツ情報の出力
//  引  数:なし
//  戻り値:なし
//------------------------------------------------
void fruit_printFruitInfo(void)
{
    E_FRUIT_KIND_ID i;

    printf("--------------------------------------------\n");

    for (i = 0; i < E_FRUIT_ID_END; i++)
    {
        printf("名前:%-16s\t価格:%5d円\n",
            gFruitTbl[i].name, gFruitTbl[i].price);
    }

    printf("--------------------------------------------\n");
}

//------------------------------------------------
//  概 要:フルーツ代金の取得。
//          フルーツIDと個数から代金を算出する
//  引  数:id  算出するフルーツID
//          num 個数
//  戻り値:0以上   算出した代金
//          負値    異常時
//------------------------------------------------
long fruit_getPriceFruit(E_FRUIT_KIND_ID id, unsigned short num)
{
    if (id < 0 || id >= E_FRUIT_ID_END)
    {
        return -1;
    }

    return gFruitTbl[id].price * num;
}

fruitで始まる関数はこのファイルで定義する。マクロ定義、構造体定義、グローバル変数は参照範囲を最小にするため本ファイル内に配置する。


fruit.h

#ifndef FRUIT_H
#define FRUIT_H
//------------------------------------------------

//------------------------------------------------
//  マクロ定義(Macro definition)
//------------------------------------------------

//------------------------------------------------
//  型定義(Type definition)
//------------------------------------------------
typedef enum
{
    E_FRUIT_ID_ORANGE   = 0,
    E_FRUIT_ID_PEACH,
    E_FRUIT_ID_BANANA,
    E_FRUIT_ID_PINE,
    //------------------------
    E_FRUIT_ID_END,   //  番兵
} E_FRUIT_KIND_ID;

//------------------------------------------------
//  プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
void fruit_printFruitInfo(void);
long fruit_getPriceFruit(E_FRUIT_KIND_ID id, unsigned short num);

//------------------------------------------------
#endif

列挙定数の定義とフルーツ情報関連のプロトタイプ宣言を配置する。