複雑なファイル分割

ファイル分割再び

C言語でプログラムを作る際、プログラムがある程度長くなってくると、ファイル分割を行う必要があると言うことは、7日目で学びました。ここで基本的な方法は説明してきましたが、プログラムが複雑になってくると、ファイル間の相関関係もまた、より複雑になってきます。そこで、最後にそういった複雑な要素の入り混じったプログラムのファイル分割について説明して、この講座の締めくくりにしましょう。

サンプルプログラム

ここでは、少し長くなりますが、実際のサンプルでのファイル分割を通してその方法を学んでいきましょう。

listex7-1:main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//	データベースに登録できる学生の最大数
#define MAX_STUDENT	10
//	学生の名前の最大の長さ
#define LENGTH		50
//	エラーメッセージの文字列の最大の長さ
#define	MESSAGE_LENGTH	256

//	エラーメッセージ
enum ERROR{
	MESSAGE_OK,
	MESSAGE_ERROR
};

//	学生のデータ
typedef struct{
	int id;				//	学生番号
	char name[LENGTH];	//	名前
}student;

//	データベースに登録されている学生の数
int num = 0;
//	学生のデータベース
student student_database[MAX_STUDENT];

int Error;

//	データベースの初期化
void initDatabase();
//	データベースへのデータの登録(学生番号、名前)
int add(int,char*);
//	学生のデータの取得
student* get(int);
//	学生データの表示
void showStudentData(student*);
//	エラーの表示
void showError();

void main(){
	int i;
	char names[][LENGTH] = {"山田太郎","太田美智子","大山次郎","山口さやか"};
	int ids[] = { 1,2,2,3 };
	initDatabase();
	for(i = 0; i < 4; i++){	//	データの登録
		add(ids[i],names[i]);
		printf("登録:%d %s\n",ids[i],names[i]);
		showError();
	}
	for(i = 0; i < 3; i++){	//	登録したデータの出力
		showStudentData(get(i+1));
	}
}

//	データベースの初期化
void initDatabase()
{
	int i;
	for(i = 0; i < MAX_STUDENT; i++){
		student_database[i].id = -1;
		strcpy(student_database[i].name,"");
	}
	Error = MESSAGE_OK;	//	エラーメッセージのクリア
	num = 0;	//	登録された学生の数を0に初期化
}

//	データベースへのデータの登録(学生番号、名前)
int add(int id,char* name)
{
	//	すでに登録されているidであれば、登録しない。
	if(get(id) == NULL && num < MAX_STUDENT){
		student_database[num].id = id;
		strcpy(student_database[num].name,name);
		num++;
		Error = MESSAGE_OK;
		//	登録できたら、1を返す。
		return 1;
	}
	Error = MESSAGE_ERROR;
	//	登録できなければ、0を返す。
	return 0;
}
//	学生のデータの取得
student* get(int id)
{
	int i;
	for(i = 0; i < num ; i++){
		if(student_database[i].id == id){	//	該当するidのデータが見つかったら
			return &student_database[i];	//	ポインタを返す
		}
	}
	return NULL;
}
//	学生データの表示
void showStudentData(student* data)
{
	if(data != NULL){
		printf("学生番号:%d 名前:%s¥n",data->id,data->name);
	}else{
		printf("データが登録されていません。\n");
	}
}
//	エラーの表示
void showError()
{
	switch(Error){
	case MESSAGE_OK:
		printf("OK!¥n");
		break;
	case MESSAGE_ERROR:
		printf("ERROR!¥n");
		break;
	}
	
}


実行結果①(入力した値が同じ場合)
登録:1 山田太郎
OK!
登録:2 太田美智子
OK!
登録:3 大山次郎
ERROR!
登録:4 山口さやか
OK!
学生番号:1 山田太郎
学生番号:2 太田美智子
学生番号:3 山口さやか

列挙型

分割する前に、新しいキーワードが出てきたので紹介します。19行目に出てくるenumが出てきます。これを、列挙型(れっきょがた)と言います。 例えば、日本語と違い英語では、月をJanuary,February,…などといった、名前で定義しています。このように、実際は名前のられるでありながら、順序などのなんらかの 秩序をもつデータを定数として定義するときに用いられるのが、この列挙型です。

例えば、以下のように使います。

enumの使用例①
enum GENDER{   // 性別の定義
  MALE,             // 男性(値は0)
  FEMALE,          // 女性(値は1)
};

このように、GENDER(性別)という名前で、MALE(男性)という定数と、FEMALE(女性)という定数が定義できます。値はそれぞれ、0,1になります。この、enumの後に来るGENDERが、列挙型であり、MALEや、FEMALEといった定数が、列挙子(れっきょし)と言います。列挙子は、通常、定義された順に、0,1,2,…と自動的に割り振られます。

尚、以下のようにすると、列挙子に任意の定数を割り振ることができます。

enumの使用例②
enum COLOR{   // 色の定義
  RED=1,        // 赤(値は1)
  BLUE=2,       // 青(値は2)
  GREEN=3,     // 緑(値は3)
};

このようにすると、任意の値を列挙子に割り振ることができます。これらは、通常の整数の定数として扱う事が可能で、switch()文などと組み合わせて使われます。

ファイル分割後のプログラム

まずは、ファイルを以下のように分割してみます。

listex7-1(分割後):main.c
#include <stdio.h>
#include "studentDatabase.h"
#include "dataOutput.h"

void main(){
	int i;
	char names[][LENGTH] = {"山田太郎","太田美智子","大山次郎","山口さやか"};
	int ids[] = { 1,2,2,3 };
	initDatabase();
	for(i = 0; i < 4; i++){	//	データの登録
		add(ids[i],names[i]);
		printf("登録:%d %s¥n",ids[i],names[i]);
		showError();
	}
	for(i = 0; i < 3; i++){	//	登録したデータの出力
		showStudentData(get(i+1));
	}
}

listex7-1(分割後):dataOutput.h
#ifndef _DATA_OUTPUT_H_
#define _DATA_OUTPUT_H_

#include "studentDatabase.h"

//	学生データの表示
void showStudentData(student*);
//	エラーの表示
void showError();

#endif // _DATA_OUTPUT_H_

listex7-1(分割後):studentDatabase.h
#ifndef _STUDENT_DATABASE_H_
#define _STUDENT_DATABASE_H_

//	データベースに登録できる学生の最大数
#define MAX_STUDENT	10
//	学生の名前の最大の長さ
#define LENGTH		50

//	エラーメッセージ
enum ERROR{
	MESSAGE_OK,
	MESSAGE_ERROR
};

//	学生のデータ
typedef struct{
	int id;				//	学生番号
	char name[LENGTH];	//	名前
}student;

//	データベースの初期化
void initDatabase();
//	データベースへのデータの登録(学生番号、名前)
int add(int,char*);
//	学生のデータの取得
student* get(int);


#endif //  _STUDENT_DATABASE_H_

listex7-1(分割後):dataOutput.c
#include "dataOutput.h"
#include <stdio.h>

//	エラーメッセージ
extern int Error;

//	学生データの表示
void showStudentData(student* data)
{
	if(data != NULL){
		printf("学生番号:%d 名前:%s¥n",data->id,data->name);
	}else{
		printf("データが登録されていません。¥n");
	}
}
//	エラーの表示
void showError()
{
	switch(Error){
	case MESSAGE_OK:
		printf("OK!¥n");
		break;
	case MESSAGE_ERROR:
		printf("ERROR!¥n");
		break;
	}
	
}

listex7-1(分割後):studentDatabase.c
#include "studentDatabase.h"
#include <string.h>

#define	MESSAGE_LENGTH	256

//	データベースに登録されている学生の数
static int num = 0;
//	学生のデータベース
static student student_database[MAX_STUDENT];
//	エラーメッセージ
int Error = MESSAGE_OK;

//	データベースの初期化
void initDatabase()
{
	int i;
	for(i = 0; i < MAX_STUDENT; i++){
		student_database[i].id = -1;
		strcpy(student_database[i].name,"");
	}
	Error = MESSAGE_OK;	//	エラーメッセージのクリア
	num = 0;	//	登録された学生の数を0に初期化
}

//	データベースへのデータの登録(学生番号、名前)
int add(int id,char* name)
{
	//	すでに登録されているidであれば、登録しない。
	if(get(id) == NULL && num < MAX_STUDENT){
		student_database[num].id = id;
		strcpy(student_database[num].name,name);
		num++;
		Error = MESSAGE_OK;
		//	登録できたら、1を返す。
		return 1;
	}
	Error = MESSAGE_ERROR;
	//	登録できなければ、0を返す。
	return 0;
}
//	学生のデータの取得
student* get(int id)
{
	int i;
	for(i = 0; i < num ; i++){
		if(student_database[i].id == id){	//	該当するidのデータが見つかったら、	
			return &student_database[i];	//  ポインタを返す
		}
	}
	return NULL;
}

では、順を追って説明していきましょう。まず、main.c以外の部分は、大きく分けて2つの部分からなっています。(表7-1)

表7-1.listex7-1の構成要素
ファイル名(.h/.c)内容関数
studentDatabase学生のデータベースinitDatabase()
add()
get()
dataOutput学生データの出力showStudentData()
showError()

この表からわかるとおり、ファイルを、データベース機能と、表示部分にわけています。このように、ファイル分割は、機能ごとに関数などの定義を振り分けるという大原則が必要になります。 どのような機能によって分割するかは、設計者の考え方次第なのですが、誰が見てもわかるような客観的な違いがあることが望ましいと言えるでしょう。以下、それぞれのパートを解説していきます。

ヘッダファイルに記述するもの

まずは、ヘッダファイルを見てみましょう。原則的に、ヘッダファイルには、外部と共有したいデータを記述するのが原則です。

まずは、studentDatabase.hを見てみましょう。ここでは、MAX_LENGTH,LENGTHといったマクロによる定数、構造体student、列挙型ERRORが記述されています。これは、これらのデータが、main.cおよび、dataOutput.cでも利用されるからです。

原則的に、studentDatabase.hは、studentDatabase.cで利用される関数を定義するのが普通ですが、このように、外部から利用する定数やマクロ、構造体なども定義します。(図7-1)

図7-1.ヘッダファイルの相関関係①
ヘッダファイルの相関関係

次に、dataOutput.hを見てみましょう。関数showStudentData()は、引数で、student型の構造体のポインタを必要としています。そのため、それが定義されたstudentDatabase.hをインクルードしています。(図7-2)

図7-2.ヘッダファイルの相関関係②
ヘッダファイルの相関関係

このように、他のヘッダファイル内で外部で定義された定数や構造体を利用する場合は、そのヘッダファイルをインクルードする必要があります。

ソースファイルの記述

続いて、ソースファイルの内容を見てみましょう。まずは、dataOutput.cから見てみましょう。関数のプロトタイプ宣言があるdataoutput.hを読み込んでいるのはもちろんのこと、 stdio.hもインクルードしています。(図7-3)

これは、関数内で、stdio.hで定義されているprintf()関数を用いているためです。この部分は、ヘッダファイルでも行っても良いのですが、 ヘッダファイルに記述すると、stdio.hの関数を利用しない他のファイルで、不必要なインクルードが起こってしまうのを避けるためです。それにより、エラーは発生しませんが、大規模なプログラムをコンパイルする場合、それにより 余計な時間がかかってしまうようなことを避けるためです。

続いて、5行目でexternを用いてErrorを定義していますが、これはstudentDatabase.cで定義されているint Errorに外部からアクセスするためのものです。 こにより、studentDatabase.c内で発生したエラーを、showError()関数内で利用することができます。

続いて、studentDatabase.cを見てみましょう。ここでは、dataOutput.c同様、複数のヘッダファイルがインクルードされています。string.hがインクルードされているのは、 strcpy関数が必要だからです。(図7-3)

図7-3.ヘッダファイルの相関関係③
ヘッダファイルの相関関係

さらに、ここには内部で使う複数の変数が定義されています。num、student_database、Errorの3つの変数は、このファイル内で利用するグローバル変数として定義されています。

Errorは、前述の通り、dataOutput.cで利用されます。ところで、残りの二つの変数についているstaticとは何でしょうか?次では、これについて解説していきましょう。

static変数

staticが対は変数を、静的変数(せいてきへんすう)と言います。この変数は、実行時に一度、呼び出しの度に初期化されない変数のことを指します。 この変数は、グローバル変数のみならず、ローカル変数でも定義できますが、一度変数の定義部分が実行去ると、何度その部分が呼び出されても、再び初期化することはありません。試しに、次のプログラムを実行してみてください。

listex7-2(変数内にローカルに宣言されたstatic変数):main.c
#include <stdio.h>

void foo();

void main(){
	int i;
	for(i = 0; i < 4; i++){
		foo();
	}
}

void foo()
{
	static int num = 0;			//	最初に一度だけ実行される
	printf("num=%d¥n",num);
	num++;
}
実行結果
num=0
num=1
num=2
num=3

14行目のfoo()関数の中で、staticなローカル変数、numが定義されています。通常のローカル変数であれば、ここで値が設定されるため、実行結果は常に、「num=0」となるはずですが、staticになると、最初の一回だけ実行されます。そのため、繰り返し呼ぶたびに値が増加していきます。

このように、静的変数は、グローバル変数と同じ領域である、静的領域に記憶されます。そのため、プログラムが終了するまで値が消えることはありません。

データの隠ぺい

では、再びstudentDatabase.cに戻りましょう。num変数とstudent_data変数にstaticがついていますが、staticがついたグローバル変数には、外部からexternでアクセスできない という性質があります。つまり、Errorと違って、外部からこれらの変数のデータをアクセスすることが出来なくなるのです。これにより、外部から変更してほしくないようなデータを外部から隠ぺいするという効果を得ることができます。つまり、externをつけるということは、外部からのアクセスを禁止するという意味になるのです。(図7-4)

図7-4.staticによるデータの隠ぺい
staticによるデータの隠ぺい

まとめ

ファイル分割の原則

最後に、main.cでは、定義された二つのヘッダファイルおよび、stdio.hをインクルードしてプログラムを実行しています。これらをまとめると、ヘッダファイルおよび.cファイルに記述すべきものの関係性および順序は以下のようになります。(表7-2)

表7-2.ヘッダファイル・ソースファイルに記入する情報(並び方は推奨する順)
ファイル記述する情報解説
ヘッダファイルヘッダファイルのインクルードヘッダファイル・ソースファイルで必要なヘッダファイル
マクロの定義外部と共有するマクロの定義
列挙型の定義外部ファイルと共有する列挙型の定義の定義
構造体の定義外部ファイルと共有する構造体の定義
関数のプロトタイプ宣言外部ファイルと共有する関数の定義
ソースファイルヘッダファイルのインクルード対応するヘッダファイルおよびその他必要なヘッダファイル
マクロの定義外部ファイルには公開しないマクロの定義
列挙型の定義外部ファイルには公開しない列挙型の定義の定義
関数のプロトタイプ宣言外部ファイルには公開しない関数の定義
構造体の定義外部ファイルには公開しない構造体の定義
グローバル変数(非static)externで外部ファイルと共有するグローバル変数
グローバル変数(static)外部ファイルと共有しないグローバル変数
関数の定義ヘッダファイルおよびファイル内で宣言されている関数の定義

これらはあくまでも原則であり、例外も多数あります。そのため、絶対にこのようにしなければならないというわけではありません。しかし、このような考え方をもってファイルの分割を 行えば、効率的にプログラミングを行う事が可能です。

最後に

以上で、発展編は終了です。このほかに、ここに記述されていないような知識もありますが、基本編も含め、ここまで学習が進めば、C言語に関する知識はかなり深くなっているといってもよいでしょう。

学習の次の段階としては、様々なOSやライブラリなどでのプログラムや、ゲームや通信など、特殊な分野でのプログラミングを学ぶことをお勧めします。また、現在ではこれらのプログラムはC言語単体で行われることは少なく、あわせてC++言語についても学ぶことも必要になります。

 → C++言語学習へ

また、プログラミングにおいては、アルゴリズムとデータ構造の学習も必要です。言語に関係なく役に立つ知識なので、プログラマーとしては必ず勉強しておきたいものです。

 → アルゴリズムとデータ構造の学習へ

さらに、これまでの知識をベースにさらなる専門知識を身につけるために、さまざまな書籍を利用して学習してみるとよいでしょう。

 → C言語の学習に役立つ書籍