複数ファイルによるプログラム | Programming Place Plus C言語編 第24章

トップページC言語編

このページの概要

以下は目次です。


規模の大きいプログラム

ここまでに登場したプログラムは、どれも1つのソースファイルだけで完結していました。しかし、もう少し規模の大きなプログラムを作るときには、ソースファイルを複数個に分割することが一般的です。

「1つのソースファイルだけで完結していた」と書いたばかりですが、これまでのプログラムも本当は複数のファイルが関わっています。これまでに登場したほとんどのプログラムで、冒頭に次の1行があります。

#include <stdio.h>

第23章で解説したとおり、これは #include というプリプロセッサディレクティブです。#include <stdio.h> という記述の意味は、「stdio.h というファイルの内容を、この位置に取り込め」というものでした。そのためこの段階で stdio.h という2つ目のファイルが関わっているといえます。

.h という拡張子を持つファイルは、一般にヘッダファイル (headef file) と呼ばれます。C言語の標準規格では単に、ヘッダ (header) と呼んでいますが、ヘッダファイルという呼び名は非常に一般的に定着しています。

【上級】標準規格では、ヘッダがファイルであることを求めていません。ヘッダファイルと呼ばないのはこのためです。また、処理系が区別できるのであれば、#include の < と > で囲まれた部分がファイル名になっている必要もありません。1

stdio.h のようなヘッダファイルは一般に、コンパイラとセットで提供されているため、コンパイラ(あるいは Visual Studio のような開発環境)をインストールしたときに自動的に作成されています。

stdio.h のように、C言語の標準規格が用意しなければならないと定めているヘッダがいくつかあり、これらのことを標準ヘッダ (standard header) と呼びます。標準ヘッダには、標準ライブラリ関数など、これまたやはり標準規格によって定められた「必要なもの一式」が記述されています。そのため、標準ヘッダを #include で取り込み、そこに含まれている機能を、標準規格の仕様のとおりに使えば、どの処理系でも共通のソースコードがコンパイルでき、同じ結果を得られるはずです。

標準ヘッダと、その中に含まれている機能の一覧が、「標準ライブラリのリファレンス(ヘッダ別)」にあります。

【上級】正確には、stdio.h などのいくつかの標準ヘッダについては用意しなくてもよいとされている環境があります。これは、フリースタンディング実行環境 (freestanding execution environment) と呼ばれ、OS が存在しないような実行環境のことをいいます。一方、OS の制御下でプログラムを実行できる環境は、ホスト実行環境 (hosted execution environment) といいます。2

ほとんどのC言語プログラムが、結果的には複数のファイルの連携で成り立っていることが分かりました。C言語では主に .c という拡張子を持つソースファイル (source file) と、主に .h という拡張子を持つヘッダファイルを作ります。実際のところ、選ぶ拡張子に制約はないので、他の拡張子であっても、その処理系が許すのならばそれで構いません。

巨大なプロジェクトになると、ヘッダファイルを含めたソースファイルの総数が数千個、あるいはそれ以上にもなることがあります。これまでの章で何度か書いてきた、「スコープは極力狭くするべき」「分かりやすい名前を付けるべき」「部品化することを考えるべき」といったガイドラインは、巨大なプロジェクトを構築し、維持管理するために非常に重要です。

ヘッダファイル

ヘッダファイルは #include によって、その内容を取り込んで使います。#include の意味は、「指定したヘッダの内容を、この記述を書いた箇所へ取り込む」ということでした。この行為をよく「ヘッダをインクルード (include) する」と表現します。ここまでの #include の使い方は、標準ヘッダをインクルードするというものだけでしたが、ヘッダファイルを自分で作ってインクルードすることもできます。

実際にヘッダファイルを作ってみます。ヘッダファイルの拡張子に制約はありませんが、一般的なやり方に従って .h とします。ここでは、utility.h という名前のヘッダファイルを作ります。

Visual Studio でヘッダファイルを作成する方法は、こちらのページにあります。

utility.h の内容は以下のようにします。

// 大きい方の値を返す
int max(int a, int b);

// 小さい方の値を返す
int min(int a, int b);

このヘッダファイルに記述されている内容は、関数プロトタイプだけです。通常、ヘッダファイルには関数の定義は書きません。定義は(拡張子 .c などの)ソースファイルの方に記述します

ヘッダファイルのイメージは、カタログとかメニュー表のようなものです。具体的な処理工程(実装)はヘッダファイルには書かないことにより、利用者の側は具体的にどのように実装されているのかを気にせず、ヘッダファイルに書かれていることだけで判断して使います。利用者側が実装に踏み込まないことで、実装を変更しやすくなる利点があります。

たとえば、printf関数を使うときには、stdio.h というカタログの中から、データを出力することに適していそうな printf関数を選んで使っています。printf関数が実際にどのように実装されているかは stdio.h には書かれていませんが、それを気にせずに使えています。

これまた “一般的には” ということで強制ではないですが、ヘッダファイルと、(拡張子 .c などの)ソースファイルを対応付けるように作ることが基本です。つまり、utility.h に対応して utility.c を作ります。そして、ヘッダファイルで宣言を書いた関数の定義を、対応するソースファイルのほうに書きます。

utility.c は次のようにしてみます。

#include "utility.h"

// 大きい方の値を返す
int max(int a, int b)
{
    if (a >= b) {
        return a;
    }
    return b;
}

// 小さい方の値を返す
int min(int a, int b)
{
    if (a <= b) {
        return a;
    }
    return b;
}

冒頭で、対応するヘッダファイルをインクルードしています。ほかのヘッダファイルのインクルードがある場合でも、まず対応するヘッダファイルからインクルードすると良いです。

なお、自分で用意したヘッダファイルをインクルードする場合は、<stdio.h> のように < と > で囲む形式ではなく、"" で囲む形式を使います。

【上級】両者の意味の詳細は処理系定義ですが3、基本的には上記のような使い分けで問題ありません。

残すは、これらの関数を実際に使う側のコードです。main関数を含んだ main.c を作成してみましょう。

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

int main(void)
{
    int a = 50;
    int b = 100;

    printf("MAX: %d\n", max(a, b));
    printf("MIN: %d\n", min(a, b));
}

実行結果:

MAX: 100
MIN: 50

ここまでに用意した max関数と min関数の定義は、utility.c に書かれていますが、#include で取り込むのは utility.h の方です。すると、utility.h に記述した関数プロトタイプが取り込まれるので、max関数と min関数の宣言が見える場所に置かれます。これで、main.c から呼び出せます。

インクルードガード

ヘッダファイルを自作し、複数のファイルが連携するプログラムをつくると、1つのヘッダファイルを重複してインクルードする可能性がでてきます。#include は、指定されたファイルの中身をそっくりそのまま取り込むだけですから、同一のヘッダファイルを何度もインクルードすると、同じ宣言や定義がくりかえし現れることになります。

特に問題なのは、ヘッダファイルからさらに別のヘッダファイルをインクルードしているケースです。たとえば、aaa.h は bbb.h をインクルードしているときに、main.c が aaa.h と bbb.h を両方ともインクルードすると、bbb.h は 2度取り込まれます。

関数宣言が重複してもエラーにはなりませんし、マクロ定義も、置換後の結果が同じであれば重複できますが、関数や変数の定義など重複できないものもあります。実際に問題が起きていないとしても、重複したインクルードによる多重定義問題への対策はつねに取るようにすべきです。問題が起こるのはヘッダファイルをインクルードした側のほうですが、対策はヘッダファイルを作った側で取れるからです。きちんと対策されたヘッダファイルを提供することが望まれます。

具体的には、すべてのヘッダファイルを次のように記述します。

// my_header.h
#ifndef MY_HEADER_H_INCLUDED
#define MY_HEADER_H_INCLUDED

// ヘッダの中身はここに書く

#endif

MY_HEADER_H_INCLUDED の部分は、ヘッダファイルごとに異なる名前を使うようにしていれば何でも構いません。確実に異なる名前を付けるために、ヘッダファイル自身の名前を元に命名することが多いです。

異なるディレクトリに同じ名前のファイルがある場合は、これだけではダメです。その可能性があるのなら、ディレクトリ名(グループ名)を付け足すなどの工夫が必要です)

処理系に依存しますが、「#pragma once」を使う方法もあります(第29章

このヘッダファイルが初めてインクルードされるときには、MY_HEADER_H_INCLUDED というマクロは定義されていない状態なので、#ifndef は真となり、内側にある記述は有効です。#ifndef の直後には、#define があり、ここで MY_HEADER_H_INCLUDED が定義されます。

2度目以降のインクルード時には、1度目のときにMY_HEADER_H_INCLUDED が定義されているので、#ifndef が偽となり、内側にある記述はすべて無視されます。こうして、2度目以降は、コードが取り込まれはするものの、中身のコードは事実上、空の状態であり、定義が重複する問題は起こらくなります。

ビルドの過程

Visual Studio では、ビルドのコマンドだけで、実行ファイルの生成まで完了します。実際にはいくつか踏まなければならない過程があって、ビルドはそれを順番に行うコマンドです。この過程の中には、プリプロセス、コンパイル、リンクといった段階があります。プリプロセスは第23章で解説したので、ここではコンパイルとリンクを説明しておきます。

コンパイル

C言語で書かれたソースファイルをコンパイルすることによって、オブジェクトファイル (object file) が生成されます。

Visual Studio であれば、ソースファイルの名前に対応した .obj という拡張子のファイルが生成されています(main.c に対して main.obj といったように)。

ソースファイルが複数あるとしても、全部一括でコンパイルしているのではなく、ソースファイルを1つずつコンパイルします。そのため、あるソースファイルをコンパイルしているとき、ほかのソースファイルの存在はみえていません。このような手法は、分割コンパイル (separate compilation) と呼びます。

main.c をコンパイルしているとき、sub.c のことは見えていないので、main.c から sub.c にある関数を呼び出すコードを書いたものの、実は sub.c にその関数の定義が記述されていなかったとしても、コンパイルエラーとしては検知できません。

// main.c

extern int f1(int x);  // sub.c にあるはずの関数の宣言

int main(void)
{
    f1(10);  // f1() の宣言がみえているのでコンパイルできる
}
// sub.c

// f1 ではなく f2 の定義。f1 の定義はどこにもない
int f2(int x)
{
    return x;
}

main.c だけをみるとコンパイル可能なコードですし、sub.c だけをみてもやはりコンパイル可能なコードです。実際、どちらのソースファイルもコンパイルは成功しますが、この次のリンクの過程で f1 の定義がないことが発覚してエラーになります。

Visual Studio でコンパイルだけを行う方法は、Visual Studio編「コンパイルを行う」のページを参照してください。


なお、ビルド時間を削減するために、最後にコンパイルされたあと、変更のないソースファイルについてはコンパイルされないのが普通です。そのため、プログラムを複数のソースファイルに分割することは、ビルド時間を減らし、開発効率を高めるという利点もあります。

リンク

コンパイルによって生成されたオブジェクトファイルは、1つのプログラムの一部分のコードに過ぎず、単体では実行できません。これらを1つにまとめて、実行できる形式のファイル(実行ファイル)を生成する必要があります。この過程をリンク (link) と呼び、リンカ (linker) というソフトウェアによって行われます。

Visual Studio にはリンカも統合されています。

各オブジェクトファイルのコードに不備があれば、リンクエラー (link error) として報告されます。たとえば先ほどの例のように、関数を呼び出そうしているが、その定義がどのオブジェクトファイルにも含まれていないことが分かると、リンクエラーになります。

extern と外部結合

今度は main.c、print.c、print.h という3つのファイルを用意します。

// main.c
#include "print.h"

int main(void)
{
    print_num(100);
    print_num(200);
    print_num(300);
}
// print.c
#include <stdio.h>
#include "print.h"

int g_last_print_num = 0;

void print_num(int num)
{
    printf("[[ %d ]]\n", num);
    g_last_print_num = num;
}
// print.h
#ifndef PRINT_H_INCLUDED
#define PRINT_H_INCLUDED

void print_num(int num);

#endif

実行結果:

[[ 100 ]]
[[ 200 ]]
[[ 300 ]]

print_num関数は、引数で指定された int型の整数を [[ ]] で囲んで出力します。また、もう1つの機能として、最後に出力した整数をグローバル変数 g_last_print_num に保存しています。

ここで、main.c から g_last_print_num を使いたいとしましょう。main.c を次のように書き換えてビルドを行ってみます。

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

int main(void)
{
    print_num(100);
    print_num(200);
    print_num(300);

    printf("%d\n", g_last_print_num);  // コンパイルエラー
}

これはコンパイルエラーになります。main.c は print.h を #include で取り込んでいますが、g_last_print_num が宣言されているのは print.c の方なので、main.c からは可視でない(第22章)ためです。グローバル変数はファイルスコープである(第22章)ことも思い出しましょう。

それなら、main.c にも g_last_print_num を宣言したらどうでしょう?

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

int g_last_print_num = 0;  // リンクエラー

int main(void)
{
    print_num(100);
    print_num(200);
    print_num(300);

    printf("%d\n", g_last_print_num);
}

今度はコンパイルは成功しますが、リンクの過程でエラーになります。g_last_print_num の定義が2箇所で重複しているのが、リンクの過程になって判明するからです。

重複した定義が許されないグローバル変数のような識別子(名前)は、外部結合(外部リンケージ) (external linkage) を持つと表現されます。

結合(リンケージ) (linkage) とは、複数の異なるスコープ、または1つのスコープ内で、2つ以上の宣言が行われている識別子が、結局のところ、何を指すのかを決定することをいいます。外部結合の場合は、複数ある宣言のすべてが1つの同じ定義を指します。ですから、外部結合の宣言は複数あっても構わないですが、定義が複数あってはならないということです。

外部結合の宣言が複数あっても構わないという部分が、今回のエラーを回避するポイントです。つまり、main.c と print.c のどちらか一方の g_last_print_num の定義が、宣言であればいいのです。グローバル変数の宣言と定義に関する仕様は複雑ですが、extern指定子を付けず、明示的に初期値を与えたものは定義であり、extern指定子を付けて、初期値を明示的に与えていなければ宣言であることを覚えておけばいいでしょう。

extern指定子は、変数を宣言する際に、型名の手前に付加します。

extern 型名 変数名;

extern は、関数宣言にも付けられます。しかし、関数宣言は暗黙的に extern が付いているものとみなされるので、省略してしまって構いません

extern 戻り値の型 関数名(仮引数の並び);


ここまでを踏まえて、main.c を書き換えます。

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

extern int g_last_print_num;  // 宣言

int main(void)
{
    print_num(100);
    print_num(200);
    print_num(300);

    printf("%d\n", g_last_print_num);
}
// print.c
#include <stdio.h>
#include "print.h"

int g_last_print_num = 0;  // 定義

void print_num(int num)
{
    printf("[[ %d ]]\n", num);
    g_last_print_num = num;
}
// print.h
#ifndef PRINT_H_INCLUDED
#define PRINT_H_INCLUDED

void print_num(int num);

#endif

実行結果:

[[ 100 ]]
[[ 200 ]]
[[ 300 ]]
300

g_last_print_num の定義をしていたところに extern を付けて、初期値を与えないようにすることで宣言に変更しました。こうすると、ビルドが成功して、実行結果の最後に 300 が出力されます。

このような方法できちんと解決できますが、あまりお勧めできる方法とはいえません。この方法では、プログラムがもっと大規模になったときに、どこかのソースファイルにあるたった1つの定義が、どこから参照されているのかを把握することが困難になる恐れがあるためです。

また、今回のサンプルプログラムでいえば、定義が書かれている print.c と、実際に使用している main.c とを結びつけているものが何もありません。唯一それらしいつながりは print.h なのですが、これがインクルードされていなくても、main.c に宣言を書いているため、g_last_print_num を使えてしまいます。

別の視点でいうと、カタログであるヘッダファイルに書かれていないはずの g_last_print_num の定義を、main.c が勝手に使っているともいえます。これは行儀が悪いプログラミングスタイルです。このようなスタイルは、C言語では昔から非常によくあるやり方ではあります。しかし、他のソースファイルに公開しても良い部分(ヘッダファイルに記述する)と、公開しない部分(ヘッダファイルに記述しない)とはきちんと意識して分けるようにしましょう。このような考え方をする癖を付けておくと、他のプログラミング言語を学ぶときに非常に役立ちます。

ではどうするかというと、次のルールを守るようにします。

このルールに従って修正すると、次のようになります。

// main.c
#include <stdio.h>
#include "print.h"  // ヘッダファイルにある宣言を使う

int main(void)
{
    print_num(100);
    print_num(200);
    print_num(300);

    printf("%d\n", g_last_print_num);
}
// print.c
#include <stdio.h>
#include "print.h"

int g_last_print_num = 0;  // 唯一の定義

void print_num(int num)
{
    printf("[[ %d ]]\n", num);
    g_last_print_num = num;
}
// print.h
#ifndef PRINT_H_INCLUDED
#define PRINT_H_INCLUDED

extern int g_last_print_num;  // 唯一の宣言

void print_num(int num);

#endif

実行結果:

[[ 100 ]]
[[ 200 ]]
[[ 300 ]]
300

print.c に定義があり、print.h に宣言があります。main.c は、print.h をインクルードすることによって、print.h に書かれている宣言を取り込んでいます。このように、ほかのソースファイルにも公開するグローバル変数を作るのなら、その宣言をヘッダファイルに書くようにしましょう。

実はもう少し改良できるポイントがあるので、次の項で取り上げます

static と内部結合

さきほどのサンプルプログラムをもう少し改良します。

グローバル変数g_last_print_num ですが、この変数の目的は「print_num関数が、最後に出力した整数を記録しておくこと」でした。最後に出力した値であることを保証するためには、print_num関数以外の場所から、g_last_print_num の値を書き換えられてはいけませんし、書き換える必要性自体がないはずです。

しかし、ヘッダファイルに宣言を公開しているため、他のソースファイルからでも g_last_print_num を書き換えることは可能です。ヘッダファイルに宣言を公開していなくても、最初の方法に戻って、main.c で extern しても同じことです。

改良の鍵は、static指定子 (static specifier) です。グローバル変数の定義に static を付加すると、結合を、内部結合(内部リンケージ) (internal linkage) に変更できます。

内部結合では、1つのスコープ内にあるすべての宣言が、たった1つの同じ定義を指します。また、定義を行ったソースファイル以外からは、その定義を使えなくなります。つまりは、ソースファイルの中に閉じ込めるような効果を持つということです。

static指定子が付加されたグローバル変数を使うと、次のように書けます。

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

int main(void)
{
    print_num(100);
    print_num(200);
    print_num(300);

    // コンパイルエラー
    // g_last_print_num は、ほかのソースファイルにあって見えない。
    printf("%d\n", g_last_print_num);
}
// print.c
#include <stdio.h>
#include "print.h"

static int g_last_print_num = 0;  // 定義。内部結合

void print_num(int num)
{
    printf("[[ %d ]]\n", num);
    g_last_print_num = num;
}
// print.h
#ifndef PRINT_H_INCLUDED
#define PRINT_H_INCLUDED

void print_num(int num);

#endif

まだコンパイルは通りません。g_last_print_num は内部結合を持つようになったため、定義を書いた print.c の中でしか使用できません。ではどうするかというと、アクセスするための関数をヘッダファイルに宣言するのです。

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

int main(void)
{
    print_num(100);
    print_num(200);
    print_num(300);

    printf("%d\n", get_last_print_num());
}
// print.c
#include <stdio.h>
#include "print.h"

static int g_last_print_num = 0;  // 定義。内部結合

void print_num(int num)
{
    printf("[[ %d ]]\n", num);
    g_last_print_num = num;
}

int get_last_print_num(void)
{
    return g_last_print_num;
}
// print.h
#ifndef PRINT_H_INCLUDED
#define PRINT_H_INCLUDED

void print_num(int num);
int get_last_print_num(void);

#endif

実行結果:

[[ 100 ]]
[[ 200 ]]
[[ 300 ]]
300

get_last_print_num関数を追加しました。例によって、宣言をヘッダファイルに、定義を(拡張子 .c などの)ソースファイルに記述します。また、main.c からは g_last_print_num を直接アクセスするのをやめて、get_last_print_num関数を呼び出すように変更します。

変数g_last_print_num は、内部結合になったため、print.c 以外の場所からは直接的にアクセスすることはできなくなりました。print.c 以外のソースファイルからは、get_last_print_num関数を通した値の取得だけが許されます。

長い時間を割いて説明してきましたが、最終的な方針としては、安易に extern を使うのはやめて、static を使おうということになります。他のファイルから値を取得したければ、取得のための関数を用意します。もし、値の書き換えもしたいというのならば、やはりそういう関数を用意します。

static と関数

static指定子を関数に付加すると、関数を内部結合にできます。つまり、その定義を書いたソースファイル内でしか呼び出せなくなります。したがって、ヘッダファイルに宣言は書きません。同じソースファイル内に宣言と定義をそれぞれ書くことは可能で、その際にはそれぞれに static指定子を付けます。

次のプログラムで確認してみましょう。

// main.c
#include "score.h"

int main(void)
{
    print_score(70);
    print_score(90);
    print_score(50);
    print_score(91);
}
// score.c
#include <stdio.h>
#include "score.h"

static void print_rank(int score);  // 宣言。内部結合

void print_score(int score)
{
    printf("SCORE: %d  ", score);
    print_rank(score);
    printf("\n");
}

static void print_rank(int score)  // 定義。内部結合
{
    printf("RANK: ");

    if (score > 90) {
        printf("S");
    }
    else if (score > 70) {
        printf("A");
    }
    else if (score > 50) {
        printf("B");
    }
    else{
        printf("C");
    }
}
// score.h
#ifndef SCORE_H_INCLUDED
#define SCORE_H_INCLUDED

void print_score(int score);

#endif

実行結果:

SCORE: 70  RANK: B
SCORE: 90  RANK: A
SCORE: 50  RANK: C
SCORE: 91  RANK: S

main.c、score.c、score.h の3つを用意します。print_score関数に得点を渡せば、それに応じた結果を出力します。

ここで、score.c の中に static指定子が付いた関数があります。print_rank関数は、引数で受け取った得点からランクを決定して出力する関数です。ランクを決定する基準(たとえば、ランクAとランクSの境目がどこにあるのかなど)を、他のファイルにまで公開する理由がなければ、このように内部結合の関数にする価値があります。

他のファイルに判定基準を公開しないことによって、後から「今の基準では、ランクA以上になることが多すぎる」などの事情で、判定基準を変えることが簡単にできます。詳しい実装方法を「非公開」にすることの価値は、こういうところにあります。


練習問題

問題① 次のプログラムの間違いを指摘してください。

// main.c
#include "sub.h"

int main(void)
{
    get_string();
    put_string();
}
// sub.c
#include <stdio.h>
#include "sub.h"

extern char g_str[80];


void get_string()
{
    fgets(g_str, sizeof(g_str), stdin);
}

void put_string()
{
    puts(g_str);
}
// sub.h
#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

extern char g_str[80];

void get_string();
void put_string();

#endif

問題② 問題①のプログラムを、extern指定子を生かす形と、static指定子を使う形の2通りに修正してください。

問題③ この章の最初の方で、max関数と min関数を持った utility.c と utility.h を作成しました。同じように、汎用的に使えそうな関数をこれらのファイルに追加し、便利な関数群を作ってください。


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第23章 プリプロセッサ)

次の章へ (第25章 配列)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る