C言語編 第23章 複数ファイルによるプログラム

この章の概要

この章の概要です。

規模の大きいプログラム

これまで登場したプログラムは、どれも1つのソースファイルだけで完結していました。 しかし、実際のプログラミングの現場においては、こんなことはまずあり得ません。

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

#include <stdio.h>

という記述が、複数のファイルを連携させるカギになります。 この記述の意味は、「stdio.h というファイルの内容を、この位置に取り込め」というものです。 .h という拡張子を持つファイルを、C言語ではヘッダファイル、あるいは単にヘッダと呼びます。

規格に厳密に従えば、< と > で囲まれた部分は、 処理系が、最終的に取り込まれる内容物を判断できるのであれば、ファイル名である必要はありません。 その点を踏まえると、ヘッダファイルと呼ぶよりも、単にヘッダと呼ぶ方が適切ではあります。

どのプログラムでも、共通して stdio.h が使えていることから分かるように、stdio.h は必ずどこかに存在しています。 普通、コンパイラメーカーが、コンパイラとセットで出荷しており、コンパイラをインストールしたときに、自動的に作成されています。
stdio.h は、C言語の規格によって、必ず用意しなければならないヘッダであると定められています。 このようなヘッダは幾つかあり、標準ヘッダと呼びます。 標準ヘッダには、標準関数など、これまたやはり規格によって定められた「必要なもの一式」が記述されています。

こうしてC言語のコンパイラは、C言語の規格が定めた標準に従って、必要なファイルや関数を用意しており、 そのおかげで、どのコンパイラでも共通のプログラムがコンパイルできるのです。

話を戻しますが、ほとんどのプログラムは複数のファイルの連携で成り立っています。 C言語であれば、主に .c という拡張子を持つソースファイルと、主に .h という拡張子を持つヘッダファイルを作り、 ヘッダファイルを #include によって取り込んで使います。 これをよく「ヘッダをインクルードする」と表現します。
巨大なプロジェクトになると、ファイルの総数が数千個に及ぶこともあります。 漠然と想像してみても、「スコープを極力狭くした方が良い」「分かりやすい名前を付けた方が良い」「うまく部品化する」といった、 これまでのガイドラインが重要そうに思えませんか?

ヘッダファイル

とはいえ、サンプルプログラムレベルで、数千個ものファイルを用意する訳にもいかず、せいぜい数個のファイルで試すことしかできません (ほぼ全ての解説サイト、解説書にとって悩ましいことです)。 どのみち、いきなり数千個レベルのプログラムを作ることなどあり得ないのですから、まずは数個のファイルによるサンプルで感覚を掴んで下さい。

まずは、自分でヘッダファイルを作ってみましょう。 ファイルそのものの作り方は、これまで作っていた .cファイルを、.hファイルに変えるだけです。
(もし今まで .c ではなく .cpp のような拡張子のファイルを作っていた人がいたら、.cpp は C++ のソースファイルの拡張子であることをお伝えしておきます。 コンパイラは C++ のつもりでコンパイルを行ってしまうでしょうから、純粋なC言語を学習する際には、.c に変えることをお薦めします)

ここでは、utility.h という名前のヘッダファイルを作り、以下のように記述します。

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

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

このヘッダファイルを #include で取り込んで使用する訳です。 このヘッダファイルに記述されている内容は、関数のプロトタイプ宣言だけです。 通常、ヘッダファイルには関数の中身は書かず、中身はソースファイル (.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;
}

先頭で utility.h を #include しています。 このように、.c と .h の両者を対応させるように作り上げていくのが一般的なスタイルです

これまで、stdio.h をインクルードする場合であれば、次のように記述しました。

#include <stdio.h>

stdio.h のような標準ヘッダをインクルードする際には、 < と > で、ヘッダファイル名を囲んで記述します
これに対し、自分で用意したヘッダファイルをインクルードする際には、次のように "" で囲みます

#include "utility.h"

本当のところ、両者の差は、どのディレクトリ(フォルダ)からヘッダファイルを探すかの違いですが、 使い分けの基準としては、先ほどの考え方で問題ありません。

前に書いたように、#include は、指定したヘッダをその位置に取り込むという効果があります。 従って、上の utility.c は、次のように書いたことになります。

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

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

/* 大きい方の値を返す */
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;
}

これで、関数のプロトタイプ宣言と定義とが、同じファイルに揃ったことになります。 残すは、これらの関数を実際に使う側の準備です。 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) );
	
	return 0;
}

実行結果:

MAX: 100
MIN: 50

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

externキーワード

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

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

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

int gLastPrintNum = 0;

void printNum(int num)
{
	printf( "[[ %d ]]\n", num );
	gLastPrintNum = num;
}
/* print.h */

void printNum(int num);

実行結果:

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

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

さて、このプログラムを改造して、main.c から gLastPrintNum の値を使えるようにしたいと思います。
まずは、そもそも main.c から gLastPrintNum はアクセスできないことを確かめておきましょう。

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

int main(void)
{
	printNum( 100 );
	printNum( 200 );
	printNum( 300 );
	
	printf( "%d\n", gLastPrintNum );  /* エラー */

	return 0;
}

これはコンパイルエラーになります。 main.c は print.h を #include で取り込んでいますが、gLastPrintNum が宣言されているのは print.c なので、main.c からは gLastPrintNum が見えません。

では、main.c にも gLastPrintNum を宣言したらどうでしょう?

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

int gLastPrintNum = 0;  /* エラー */

int main(void)
{
	printNum( 100 );
	printNum( 200 );
	printNum( 300 );
	
	printf( "%d\n", gLastPrintNum );

	return 0;
}

これもエラーになります。

これはコンパイルエラーではなく、リンクエラーですが、とにかくエラーです。

このエラーの理由は、同じ名前の変数が複数のソースファイルに登場したことによります。 先ほど試したように、main.c からは print.c に宣言した gLastPrintNum は見えていませんが、その事実とは無関係に、 同じ名前での重複宣言は許可されません。
これは、(後で登場する静的グローバル変数を除いて)グローバル変数が外部結合と呼ばれる結合規則を持っているためです。 要するに、定義のあるソースファイルの外側=他のファイルにまで、名前が公開されてしまうということです。

外部結合で公開される名前を外部名と呼びますが、古い環境では、先頭 6文字分だけしか有効とはならず、7文字目以上が切り取られてしまう場合があります。 古い規格の頃から存在するC言語の標準関数の名前が、半ば無理やりに 6文字以内に収められているのはそのためです。

ここで、この節の主題である externキーワードが登場します。 次のように書き換えます。

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

extern int gLastPrintNum;

int main(void)
{
	printNum( 100 );
	printNum( 200 );
	printNum( 300 );
	
	printf( "%d\n", gLastPrintNum );

	return 0;
}

実行結果:

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

今度は成功しました。 実行結果の最後に、ちゃんと 300 が出力されています。

externキーワードを付けて変数を宣言すると、その変数の本物の実体は、別のところに存在していることを表現できます。 つまり、extern付きで宣言した main.c の gLastPrintNum は、実はそこにはなく、実体は extern の付いていない方の gLastPrintNum です。 extern が付いていない方というのは、もちろん、print.c で宣言されている gLastPrintNum です。
従って、extern を使うのなら、extern が付かない本物の実体がどこかに必要になります。 また、ある1つの本物の実体に対して、extern付きの宣言が複数存在しても構いません

なお、externキーワードは変数だけでなく、関数の宣言時(プロトタイプ)にも付けることができます。 しかし、関数の場合には、extern の有無は何ら影響を与えないので、省略してしまって構いません


さて、externキーワードを使って解決しましたが、本当のところ、先ほどの方法はあまりお薦めしません。 この方法に慣れきってしまうと、至る所で extern付き宣言を書いてしまい、収集がつかなくなります。
また、今回の例でいえば、print.h を #include していなくても、extern による宣言さえあれば、 gLastPrintNum にアクセスできてしまうため、ますます不明瞭だと言えます。

そこで、extern付きの宣言は、実体を定義している .cファイルに対応する .hファイルに書くようにします

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

int main(void)
{
	printNum( 100 );
	printNum( 200 );
	printNum( 300 );
	
	printf( "%d\n", gLastPrintNum );

	return 0;
}
/* print.c */
#include <stdio.h>
#include "print.h"

int gLastPrintNum = 0;

void printNum(int num)
{
	printf( "[[ %d ]]\n", num );
	gLastPrintNum = num;
}
/* print.h */

extern int gLastPrintNum;

void printNum(int num);

実行結果:

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

main.c からは print.h を #include で取り込んでいるのですから、main.c から extern付きの宣言はちゃんと見えます。

静的グローバル変数

extern のところで使ったサンプルプログラムをもう少し改良します。

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

しかし、extern宣言を行うことによって、他のファイルからでも gLastPrintNum にはアクセスし放題になってしまい、 例えば、main.c から書き換えることも可能になってしまいます。
かといって、extern宣言をやめると、main.c から全くアクセスできなくなってしまうのでした。

そこで今度は、前章でも登場した staticキーワードを使います。 前章では、静的ローカル変数を作るために使いましたが、今回は静的グローバル変数を作るために使います。

static を付けてグローバル変数を定義すると、そのグローバル変数は、そのファイルの中でしか使えなくなります。 同一ファイルからだけアクセスできるこのスコープを、ファイルスコープと呼びます。
また、静的グローバル変数は内部結合という結合規則を持ち、これは外部のファイルには名前を公開しないというものです。 ですから、同じ名前を別のファイルで使っても問題ありません。

静的ローカル変数と、静的グローバル変数は、いずれも「静的」ですし、いずれも「static」を付けますが、両者の意味には何ら関係がありません。 切り離して考えた方が良いです。

静的グローバル変数を使うと、次のようになります。

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

int main(void)
{
	printNum( 100 );
	printNum( 200 );
	printNum( 300 );
	
	printf( "%d\n", gLastPrintNum );

	return 0;
}
/* print.c */
#include <stdio.h>
#include "print.h"

static int gLastPrintNum = 0;

void printNum(int num)
{
	printf( "[[ %d ]]\n", num );
	gLastPrintNum = num;
}
/* print.h */

void printNum(int num);

まだコンパイルは通りません。 先ほど説明したように、静的グローバル変数はファイルスコープを持つので、main.c からアクセスすることは不可能です。 ではどうするかというと、アクセスするための関数を用意するのです。

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

int main(void)
{
	printNum( 100 );
	printNum( 200 );
	printNum( 300 );
	
	printf( "%d\n", getLastPrintNum() );

	return 0;
}
/* print.c */
#include <stdio.h>
#include "print.h"

static int gLastPrintNum = 0;

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

int getLastPrintNum(void)
{
	return gLastPrintNum;
}
/* print.h */

void printNum(int num);

int getLastPrintNum(void);

実行結果:

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

getLastPrintNum関数を追加しました。 例によって、プロトタイプ宣言をヘッダファイルに、定義をソースファイルの側に記述します。 また、main.c からは gLastPrintNum を直接アクセスするのをやめにして、getLastPrintNum関数を呼び出すように変更します。

これで完璧です。 変数gLastPrintNum は、ファイルスコープを持つため、print.c 以外の場所からは不用意にアクセスすることは許されなくなり、 その管理を printNum関数だけに任せられます(もちろん、print.c にある他の関数からは代入操作などを行えてしまいますが、 こればかりはC言語ではどうにもできません)。
また、print.c 以外の場所からは、getLastPrintNum関数を通じて、値の取得だけが許されます。 この関数を作っても、値を書き換えることはできないというのは重要な点です。他のファイルに許可されたのは取得だけなのです。


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

静的関数

変数に staticキーワードを付ければ静的変数になるように、関数なら静的関数になります。

関数に対する staticキーワードには、その関数を内部結合にする効果があります。 したがって、そのソースファイル内でしか呼び出せない関数になります。
内部結合なので、静的関数のプロトタイプ宣言をヘッダファイルに書いてはいけません。 プロトタイプ宣言と定義は、いずれもソースファイル側に記述し、それぞれの先頭に staticキーワードを付けます。

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

int main(void)
{
	printScore( 70 );
	printScore( 90 );
	printScore( 50 );
	printScore( 91 );

	return 0;
}
/* score.c */
#include <stdio.h>
#include "score.h"

static void printRank(int score);

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

static void printRank(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 */

void printScore(int score);

実行結果:

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

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

ここで、score.c の中に、静的関数が登場しています。 静的関数printRank は、得点を元にして、ランクを決定し、そのランクを出力する関数です。 ランクを決定する基準(例えば、ランクAとランクSの境目がどこにあるのか等)を、他のファイルにまで公開する理由がなければ、 このように静的関数にする効果があります。 (判定基準が組織の外部には非公開、なんてことはよくある話ですよね)

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


練習問題

問題@ 次のプログラムの間違いを指摘して下さい。

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

int main(void)
{
	getString();
	putString();

	return 0;
}
/* sub.c */
#include <stdio.h>
#include "sub.h"

extern char gStr[81];


void getString()
{
	fgets( gStr, sizeof(gStr), stdin );
}

void putString()
{
	puts( gStr );
}
/* sub.h */
extern char gStr[81];

void getString();
void putString();

問題A 問題@のプログラムを、externキーワードを生かす形と、staticキーワードを使う形の2通りに修正して下さい。

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


解答ページはこちら

参考リンク

更新履歴

'2011/3/29 「静的グローバル変数」のサンプルプログラム中で、セミコロンが抜けていたのを修正。

'2011/2/5 #include において、< と > で囲まれた文字の並びが、ファイル名であるとは限らない点について補足。

'2009/7/27 「静的関数」のサンプルプログラム中で、インクルードするファイル名が間違っていたのを修正。

'2009/7/25 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ