ソースコードの分割

今までのサンプルコードは、ひとつのソースファイルに全てのコードを記述していました。
短いプログラムならばそれで十分ですが、コードが長くなるとたくさんスクロールしなくてはならなくなり、読みにくくなります。

また、大規模なプログラムとなると複数人で作業することが普通にあります。
複数人で作業といっても同じソースコードを同時に編集するのではなく、プログラムを機能ごとに分割し、それぞれが別々のソースコードを作成し、最後に合体させることでプログラムを作ります。

そのためにはソースコードを分割する必要があります。
このページではソースコードの分割方法を説明します。

ソースの分割方法

今回は以下のサンプルコードを複数のファイルに分割してみます。
コード中で定義されている関数は、C言語の標準関数randを少し使いやすくするための関数群です。

数学関数については数学関数を参照してください。
ランダム値については乱数(ランダム値)の生成を参照してください。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>

//プロトタイプ宣言
void Init(); 
int GetRandom(int);
int GetRandomRange(int, int);
double GetRandomDouble();
double GetRandomDoubleRange(double, double);

void Init()
{
    //関数内のstatic変数は
    //その関数からしかアクセスできない
    //初期化しないと自動的に0で初期化される
    static char isInit;

    //この関数はプログラム中で一度しか呼び出しに意味を持たない
    if(isInit != 0)
        return;
    isInit = 1;
    
    srand((unsigned)time(NULL));
}

//0からmaxを含むランダム整数を返す
int GetRandom(int max)
{
    Init();
    if (max < 2)
        return 0;
    return rand() % (max + 1);
}

//minからmaxを含むランダム整数を返す
int GetRandomRange(int min, int max)
{
    if (min > max)
    {
        int tmp = min;
        min = max;
        max = tmp;
    }
    return GetRandom(max - min) + min;
}

//0から1未満のランダム小数を返す
double GetRandomDouble()
{
    Init();
    return (double)rand() / RAND_MAX;
}

//minからmax未満のランダム小数を返す
double GetRandomDoubleRange(double min, double max)
{
    if (min > max)
    {
        double tmp = min;
        min = max;
        max = tmp;
    }
    double d;
    if (min < 0 && max > 0)
        d = fabs(min) + max;
    else
        d = fabs(max - min);

    return GetRandomDouble() * d + min;
}

int main()
{
    //省略
}

ソースファイルの追加

ソースの分割のためには、新しいソースコードをプロジェクトに追加する必要があります。
追加の方法の詳細は基礎知識編のプロジェクトを作るを参考にしてください。

ソースファイルの追加

新しい項目の追加ダイアログでは「Visual C++」→「C++ファイル(cpp)」を選択して、名前を付けて「追加」をクリックします。
ファイル名は何でも構いませんが、ファイルの中身が分かるような名称にしましょう。
今回は「random.c」という名称とします。
ついでに、main関数を記述するほうのソースファイルの名称は「main.c」に変更しておきます。
(名前の変更はファイルを右クリック→「名前の変更」で可能)

Visual C++ではC言語とC++言語を混在させたソースを書けますが、ソースファイルの拡張子を「.c」とすると、C++の機能は使えなくなり、純粋なC言語として扱われます。

新しく作った「random.c」に、main以外の関数と関数のプロトタイプ宣言を移動させます。
rand関数とtime関数、数学関数の実行に必要なインクルードファイルの記述も移動させます。
(stdlib.h、time.h、math.h)

これから作る「random.c」をライブラリとして使用する場合、「stdlib.h」「time.h」「math.h」も自動的にインクルードされることになるので、「main.c」にこれらのインクルードがなくても数学関数などは利用可能になります。
しかし「main.c」からもこれらの標準関数を利用するなら、「main.c」にもインクルードを記述して構いません。
(標準関数などの大抵のライブラリは二重にインクルードしても大丈夫な作りになっています)

//random.c
#include <stdlib.h>
#include <time.h>
#include <math.h>

//プロトタイプ宣言
void Init(); 
int GetRandom(int);
int GetRandomRange(int, int);
double GetRandomDouble();
double GetRandomDoubleRange(double, double);

void Init()
{
    //省略
}

//0からmaxを含むランダム整数を返す
int GetRandom(int max)
{
    //省略
}

//minからmaxを含むランダム整数を返す
int GetRandomRange(int min, int max)
{
    //省略
}

//0から1未満のランダム小数を返す
double GetRandomDouble()
{
    //省略
}

//minからmax未満のランダム小数を返す
double GetRandomDoubleRange(double min, double max)
{
    //省略
}
//main.c
#include <stdio.h>

int main()
{
    //省略
}

このようにファイルが分割されます。
長くなるので関数の実装は省略しています。

「random.c」のほうでは標準入出力などは使用しないので、「stdio.h」のインクルードは必要ありません。

ヘッダファイルの追加

ソースコードを分割しただけではまだ不十分です。
「random.c」にどのような関数や変数があるのかを伝えるためにヘッダファイル(ヘッダーファイル)というものを新しく作成します。

ヘッダファイルは「stdio.h」などと同じく、外部で定義されている関数や変数などの情報が記述されたファイルです。
関数を使う側はヘッダファイルをインクルードすることで、外部定義の関数の情報を知ることができるようになります。

ヘッダファイルの追加方法はソースファイルの追加方法とほぼ同じです。
異なるのは、ソリュージョンエクスプローラーから「ヘッダーファイル」の方を右クリックして追加する点と、新しい項目追加ダイアログで「ヘッダーファイル(.h)」を選択する点です。

ヘッダファイルの追加1
ヘッダファイルの追加2

名前は「random.h」とします。
名前は(拡張子以外は)ソースコードと同じにしておきます。
ヘッダファイルの拡張子は「.h」です。

さて、ヘッダーファイルには外部に公開したいものの情報を記述します。
具体的には関数のプロトタイプ宣言や構造体、マクロなどです。
今回は関数だけなので、公開すべき関数のプロトタイプ宣言だけを「random.c」から「random.h」に移動させます。

//random.h

//外部に公開する関数のプロトタイプ宣言
int GetRandom(int);
int GetRandomRange(int, int);
double GetRandomDouble();
double GetRandomDoubleRange(double, double);
//random.c
#include <stdlib.h>
#include <time.h>
#include <math.h>

//外部に公開しない関数のプロトタイプ宣言
//staticをつけておく
static void Init(); 

//関数の実際の定義自体はソースファイルに記述する

static void Init(){ //省略 }

//0からmaxを含むランダム整数を返す
int GetRandom(int max){ //省略 }

//minからmaxを含むランダム整数を返す
int GetRandomRange(int min, int max){ //省略 }

//0から1未満のランダム小数を返す
double GetRandomDouble(){ //省略 }

//minからmax未満のランダム小数を返す
double GetRandomDoubleRange(double min, double max){ //省略 }
//main.c
#include <stdio.h>

int main(){ //省略 }

関数Initはrandom.cの関数群から以外はアクセスされることのない関数です。
このような公開しない関数にはstatic修飾子をつけ、プロトタイプ宣言はヘッダでは行わずソースファイル内で行います。
staticが付けられた関数や変数はそのファイル内からのみアクセスが可能になります。

staticの項では、関数内で宣言したstatic変数について説明しました。
関数内で宣言される変数にstaticを付けると、「その関数内からのみアクセス可能」かつ「静的領域に保存される(プログラムの開始から終了まで生存する)」という変数になります。
これと同じことで、関数の外にあるもの(つまり関数やグローバル変数)にstaticを付けると、アクセス可能な範囲が「そのファイル内」に限定されることになります。
(寿命はもともと「プログラムの開始から終了まで」なので変わりません)

外部に公開されないので、「Init」などというよく使われそうな名前を付けても名前の衝突が起こることはありません。

インクルードガード

この作成したヘッダファイルをソースコードにインクルードするのですが、まだ完成ではありません。

C言語では、関数などを再度定義しなおしてはならないというルールがあります。

C言語の#includeは「その場所に指定のファイルの内容をすべて展開する」という意味になります。
なので、インクルードしたファイル内の関数などが使えるわけです。

先ほど作成した「random.h」をそのままインクルードして使用する場合、ひとつのソースファイルからのみインクルードするのであれば問題は起こりません。
しかしプログラムの規模が大きくなってくると、ソースファイルを複数に分割することになり、いろいろなソースファイルから同じライブラリをインクルードして使用することになります。
そのようなプログラムで「random.h」を使用すると、同じ定義が複数のソースファイル内で行われることとなり、二重定義になってしまいます。

そのため、複数のソースからインクルードしても問題が発生しないようにする必要があります。
それをインクルードガードといいます。

最も簡単な方法は、ヘッダファイルの先頭に#pragma onceを記述する方法です。

#pragma once

//random.h

int GetRandom(int);
int GetRandomRange(int, int);
double GetRandomDouble();
double GetRandomDoubleRange(double, double);

これだけで、このヘッダファイルは二重に読み込まれることはなくなります。
VC++でヘッダファイルを追加すると自動的に先頭に記述されていると思います。

#pragma onceはC言語標準の機能ではありませんが、多くのコンパイラでサポートされているので、大抵は問題なく使用できます。

その他の方法としては、#defineと#ifdef、#ifndefを利用してインクルード済みか否かを判断する方法です。

//random.h

#ifndef INCLUDED_RANDOM
#define INCLUDED_RANDOM

int GetRandom(int);
int GetRandomRange(int, int);
double GetRandomDouble();
double GetRandomDoubleRange(double, double);

#endif

#ifndefは、指定の値がdefineされていなければ#endifまでの処理を行います。
(if not define)
#ifdefはその逆です。
これらも#defineと同じくプリプロセッサなので、コンパイル前に処理されます。

つまり「INCLUDED_RANDOM」というマクロが存在しなければ「INCLUDED_RANDOM」を定義し、ヘッダファイルの定義を行います。
このヘッダファイルが再度読み込まれたときには「INCLUDED_RANDOM」がすでに定義されているので、ヘッダファイルの処理はスキップされます。

この方法によるインクルードガードは、マクロ名が他と重複しないように気を付ける必要があります。
特にアンダースコア(_)から始まるマクロ名はC++では予約済となっているため使用は避けたほうが良いです。

この「random.h」は関数のプロトタイプ宣言しかないので、実は二重にインクルードしても問題は起こりません。
プロトタイプ宣言は「定義」ではなく「宣言」なので、二重定義にはならないのです。
ソースコード上に同じ関数名のプロトタイプ宣言を重複させても問題なくコンパイルは通ります。

作成したヘッダファイルのインクルード

最後に、先ほど作成したヘッダファイルを「main.c」でインクルードします。

//main.c
#include <stdio.h>
#include "random.h"

int main(){ //省略 }

これで「random.h」が「main.c」に読み込まれ、「random.h」に記述されている関数や変数などにアクセスができるようになります。

「stdio.h」などのC言語標準ライブラリのインクルードの場合はヘッダファイル名を「<>」で囲いますが、自作したヘッダファイルをインクルードする場合には「""」で囲います。
これはそのヘッダファイルが存在する場所によって変わるのですが、「自作のヘッダファイルの場合はダブルクォーテーション」という認識で問題ありません。

グローバル変数の扱い

グローバル変数は、そのプログラム中のどこからでもアクセスできる変数です。
ソースファイルを分割していてもすべてのソースファイルからアクセス可能ですが、少し注意点があります。

ソースファイルごとに独立してグローバル変数は使用できますが、名前の重複に気を付ける必要があります。

//test.c

int global;
//main.c

int global;

上のサンプルコードの場合は、どちらのファイルからも問題なくアクセスすることは可能です。
しかし以下のようにするとエラーになります。

//test.c

int global = 1;
//main.c

int global = 1;

同じことをしているので問題ないように見えますが、C言語では二重宣言は許されていますが二重定義は許されていません。
「変数globalは1である」という「定義」は一回しか行えません。

変数の初期化を行うと変数の定義を行ったことになります。
宣言だけの場合や、宣言後の代入は定義にはなりません。

グローバル変数はプログラム中で共通して使用されるもので、どこかにひとつだけ「実体」が存在します。
「実体」が外部ファイルに存在することを示すにはextern修飾子を使用します。

//test.c

//変数globalの実体
int global = 1;
//main.c

//test.cにある変数globalにアクセスする
extern int global;

externが付けられた変数は、そのソースファイル中には実体は存在せず外部ファイルにある、という意味になります。

実はデフォルトではグローバル変数はexternが付けられているものとみなされます。
なので省略は可能なのですが、外部変数であることを明確にするためには付けたほうが良いでしょう。

同じく関数もデフォルトでexternが付けられているものとみなされます。

全てのファイルでexternを付けると、実体が存在しないことになりエラーになります。

//test.c

extern int global;
//main.c

extern int global;

ただし、externを付けていても初期化を行うと実体を定義したことになり、以下のような場合はエラーになりません。
(ややこしいのでお勧めしませんが)

//test.c

extern int global = 1;
//main.c

extern int global;

関数内のローカル変数はもともと関数外からは見えないので、外部ファイルの事は気にする必要はありません。

ヘッダファイルにexternを記述

上の例ではソースファイル(.cファイル)にextern宣言を記述していましたが、通常はどこかひとつのソースファイルに実体を記述し、そのヘッダファイルにextern宣言を記述しておきます。

//test.h

extern int global;
//test.c

//変数globalの実体
int global = 1;
//main.c

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

int main()
{
    printf("%d", global);
}

「main.c」ファイルにはexternの記述がありませんが、「test.h」をインクルードしており、そこに変数globalのextern宣言が記述されているのでアクセスが可能となります。

グローバルなstatic変数

グローバル変数にstaticを付けるとそのファイル内からのみアクセスが可能な変数となります。
上で説明したstatic関数と同じことです。

//test.c

//test.c内からのみアクセスできる変数
static int global = 1;

void something()
{
    //アクセス可能
	global = 2;
}
//main.c

//test.cとは別の変数とみなされる
int global;

//externをつけてもダメ
extern int global;

この変数は静的グローバル変数と呼ばれます。
staticは「宣言された領域外からはアクセス不可にする(内側からは可能)」「寿命をプログラムの開始から終了までにする」と覚えておきましょう。

ファイル分割を利用したライブラリの例

最後に、ファイル分割を利用したrandomライブラリの例を注釈付きで掲載しておきます。

//main.c

#include <stdio.h>
//random.hをインクルード
#include "random.h"

//stdlib.hやtime.hなどのインクルードは必要ない

int main()
{
    //非公開関数にはアクセスできない
    //Init();

    //外部関数を使用する
    printf("GetRandom(5)\n");
    for (int i = 0; i < 10; i++)
        printf("%d\n", GetRandom(5));

    printf("\nGetRandomRange(-100, 0)\n");
    for (int i = 0; i < 10; i++)
        printf("%d\n", GetRandomRange(-100, 0));

    printf("\nGetRandomDouble()\n");
    for (int i = 0; i < 10; i++)
        printf("%f\n", GetRandomDouble());

    printf("\nGetRandomDoubleRange(-1.0, -3.0)\n");
    for (int i = 0; i < 10; i++)
        printf("%f\n", GetRandomDoubleRange(-1.0, -3.0));

    //外部変数にアクセスしてみる
    printf("\npublicValue: %d\n", publicValue);
    publicValue = 10;
    printf("GetPublicValue(): %d\n", GetPublicValue());

    //外部関数を通して非公開変数にアクセス
    SetPrivateValue(20);
    printf("GetPrivateValue(): %d\n", GetPrivateValue());

    getchar();
}
//random.c
/*
    C言語のrand関数をラップするライブラリのサンプル
*/

#include <stdlib.h>
#include <time.h>
#include <math.h>

//statcを付けた変数、関数は
//random.c以外からは見えない

//非公開関数のプロトタイプ宣言
static void Init();

//非公開変数
//このライブラリには必要ないが
//説明のために記述
static int privateValue;

//公開変数
//これもライブラリには必要なし
int publicValue;

//非公開関数
//乱数の初期化処理
//static変数を利用して一度だけ初期化されるようにしている
void Init()
{
    static char isInit;
    if(isInit != 0)
        return;
    isInit = 1;
    
    srand((unsigned)time(NULL));
}

//以降は公開関数
//他の関数との名前被りには気を付ける

//0からmaxを含むランダム整数を返す
int GetRandom(int max)
{
    Init();
    if (max < 2)
        return 0;
    return rand() % (max + 1);
}

//minからmaxを含むランダム整数を返す
int GetRandomRange(int min, int max)
{
    if (min > max)
    {
        int tmp = min;
        min = max;
        max = tmp;
    }
    return GetRandom(max - min) + min;
}

//0から1未満のランダム小数を返す
double GetRandomDouble()
{
    Init();
    return (double)rand() / RAND_MAX;
}

//minからmax未満のランダム小数を返す
double GetRandomDoubleRange(double min, double max)
{
    if (min > max)
    {
        double tmp = min;
        min = max;
        max = tmp;
    }
    double d;
    if (min < 0 && max > 0)
        d = fabs(min) + max;
    else
        d = fabs(max - min);

    return GetRandomDouble() * d + min;
}

//これ以降はrandomライブラリには必要ないが
//説明のために記述した公開関数

//公開変数を取得
//関数を通さずに直接公開変数にアクセスも可能
int GetPublicValue()
{
    return publicValue;
}

//非公開変数を取得
int GetPrivateValue()
{
    return privateValue;
}

//非公開変数に値をセット
void SetPrivateValue(int n)
{
    privateValue = n;
}

//random.h
#pragma once

//公開する関数のプロトタイプ宣言
//公開しない関数のプロトタイプ宣言は
//ソースファイルのほうに記述する

//Visual Studioでは
//関数の前に関数の説明をコメントで書くと
//呼び出し側からコメントが読める

//プロトタイプ宣言に引数名まで記述すると
//呼び出し側から引数名も参照できるので
//引数指定が分かりやすくなる


//0からmaxを含むランダム整数を返す
int GetRandom(int max);

//minからmaxを含むランダム整数を返す
int GetRandomRange(int min, int max);

//0から1未満のランダム小数を返す
double GetRandomDouble();

//minからmax未満のランダム小数を返す
double GetRandomDoubleRange(double min, double max);


//公開変数を取得
//関数を通さずに直接公開変数にアクセスも可能
int GetPublicValue();

//非公開変数を取得
int GetPrivateValue();

//非公開変数に値をセット
void SetPrivateValue(int x);

//外部変数の宣言
//ライブラリ利用側に外部変数の存在を明示する
extern int publicValue;

//※
//コンパイラによってはエラーになるので
//ヘッダファイルやソースファイルの
//末尾は必ず改行で終わるようにする

Visual Studioでは関数のプロトタイプ宣言の前にコメントを書くと、関数の利用側からそのコメントを読めるようになります。
また、プロトタイプ宣言の引数にデータ型だけでなく引数名も記述しておくと、関数呼び出し側から引数名が参照できるようになり、引数の意味が分かりやすくなります。

コンパイルはソースファイル単位で行われるため、例えば「main.c」を修正しても「random.c」の再コンパイルは行われません。
ソースファイルの規模が大きくなってくるとコンパイルもそこそこ時間のかかる作業になりますので、独立性の高い機能は別ファイルに分割しておくと時間の短縮にもなります。

gccでの分割ソースコードのコンパイル例

ここではgccを用いて分割したソースコードのコンパイル例を示しておきます。

VisualStudioなどはボタンひとつで開発環境がコンパイルとリンクの作業を行ってくれますが、コンパイラにソースファイルを直接指定してコンパイルする場合はリンク作業も自分で行う必要があります。

ソースファイルの分割やヘッダファイルの作成は上記の手順通りで問題ありません。
ここでは上記と同じ「main.c」「random.c」「random.h」の三つのファイルを用意します。

ファイルの追加は新規テキストファイルを手動で追加し、名前を適切に変更するだけです。
ソースファイルとヘッダーファイルは同じ場所に配置しても構いませんし、フォルダで分けたほうが良いならばそのようにしてください。

gccでのコンパイルはソースファイルの指定を一つ増やすだけです。
例えばまとめてコンパイルして実行ファイルを生成する場合は

$ gcc -o test main.c random.c

とします。

先頭の「$」はコマンド入力であることを示すもので、実際に入力する必要はありません。

-oオプションは指定したファイル名でファイルを出力します。
上記のようにすれば二つのソースファイルから「test」という実行ファイルを生成します。
ヘッダファイルの指定は必要ありません。
(カレントディレクトリからのファイルへのパスは適切に設定してください)

しかし一括コンパイルはすべてのソースファイルをコンパイルしますので、変更していないファイルもコンパイルされるため時間がかかります。
-cオプションでコンパイルすると実行ファイルではなくオブジェクトファイル(○○.o)が生成されます。

$ gcc -c main.c
$ gcc -c random.c

//一括してオブジェクトファイルを生成
$ gcc -c main.c random.c

修正が発生したソースファイルだけコンパイルし直せば時間の短縮になります。

-cオプションでのコンパイルは実行ファイルではなくオブジェクトファイルを生成します。
プログラムは複数のオブジェクトファイルを結合し、実行ファイルを生成します。

オブジェクトファイルから実行ファイルを生成する場合も-oオプションでオブジェクトファイルを(必要ならば複数個)指定します。

$ gcc -o test main.o random.o

今回は必要ありませんが、math.h内の関数を使用する場合は実行ファイル生成時に-lmオプションを付けます。

$ gcc -o test main.o random.o -lm

こういった指定は毎回行うのは面倒なので、makefileを作成するのが一般的です。

ソースファイルなどと同じ場所に、以下の内容で「makefile」という名前で保存します。

test: main.o random.o
	gcc -o test main.o random.o
main.o: main.c
	gcc -c main.c
random.o: random.c
	gcc -c random.c

2、4、6行目の先頭の空白は空白文字ではなくタブ文字で記述します。

そして「make」コマンドを実行すれば「test」という実行ファイルが作成されます。

$ make

makefileの書き方はいろいろありC言語の話からは外れるので、興味がある人は調べてみてください。