一週間で身につくC言語の基本

おさえておきたいプログラミングの基本

【応用編:7 複雑なファイル分割

基本編で学んだファイル分割を極めましょう。

7-1.複雑なファイル分割

(1) ファイル分割再び

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

(2) 分割前のファイル

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

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();

int main(int argc,char** argv){
    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));
	}
    return 0;
}

//	データベースの初期化
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 山口さやか

(3) プログラムの処理内容

ファイル分割を行う前に、プログラムの簡単な仕組みを説明していきましょう。

このプログラムは、簡易学生データベースです。学生データは、学生番号(id)と名前(name)から構成されています。個の学生データは、以下の構造体studentに格納されます。

学生データを格納する構造体
typedef struct{ int id; // 学生番号 char name[LENGTH]; // 名前 }student;

学生の名前を表すnameは配列となっており、長さはLENGTH(=50)となっています。

データベースの機能は関数として提供されています。関数各々は以下のような役割を担っています。(表7-1)

表7-1.listex7-1の関数の一覧
関数引数戻り値意味
initDatabase()なしなしデータベースを初期化
add()int id,char* name成功:0 失敗:1指定した学生番号(id)の学生の名前(name)を登録
get()int idstudent*指定した学生番号の情報取得(該当無しならNULL返す)
showStudentData()student*なし指定した学生のデータを表示
showError()なしなしエラーメッセージの表示

main関数の前半でデータベースを初期化し、4名の学生の情報を登録しています。(43~51行目)

登録に成功すると「OK!」、失敗すると「ERROR!」が表示されます。学生のidが重複するか、登録できる最大の学生数(MAX_STUDENT)を超えると、エラーになります。

ここでは、あえて学生番号「2」が重複する学生のデータを登録し、最初は成功、2回目はエラーが出て失敗することを示しています。

後半では、データベースに登録している学生の一覧を取得しています。(52~54行目)学生番号が1,2,3の学生の番号の学生番号と名前を表示しています。

(4) 列挙型

分割する前に、新しいキーワードが出てきたので紹介します。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()文などと組み合わせて使われます。

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

listex7-1ファイルを以下のように分割してみます。

listex7-2:main.c
#include <stdio.h>
#include "studentDatabase.h"
#include "dataOutput.h"

int main(int argc, char** argv) {
    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));
    }
    return 0;
}
listex7-2: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.h
#ifndef _DATA_OUTPUT_H_
#define _DATA_OUTPUT_H_
    
#include "studentDatabase.h"
    
//	学生データの表示
void showStudentData(student*);
//	エラーの表示
void showError();
    
#endif // _DATA_OUTPUT_H_
listex7-2: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;
}
listex7-2: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;
    }
}

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

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

この表からわかるとおり、ファイルを、データベース機能と、表示部分にわけています。このように、ファイル分割は、機能ごとに関数などの定義を振り分けるという大原則が必要になります。

どのような機能によって分割するかは、設計者の考え方次第なのですが、誰が見てもわかるような客観的な違いがあることが望ましいと言えるでしょう。以下、それぞれのパートを解説していきます。

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

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

まずは、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)

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

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

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

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

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

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

(7) static変数

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

listex7-3:main.c
#include <stdio.h>

void foo();

int main(int argc, char** args) {
    int i;
    for(i = 0; i < 4; i++){
        foo();
    }
    return 0;
}

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

foo()関数の中で、staticなローカル変数、numが定義されています。(14行目)

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

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

(8) データの隠蔽

では、再びstudentDatabase.cに戻りましょう。num変数とstudent_data変数にstaticがついていますが、staticがついたグローバル変数には、外部からexternでアクセスできないという性質があります。

つまり、Errorと違って、外部からこれらの変数のデータをアクセスすることが出来なくなるのです。

これにより、外部から変更してほしくないようなデータを外部から隠ぺいするという効果を得ることができます。つまり、externをつけるということは、外部からのアクセスを禁止するという意味になるのです。(図7-4)

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

(9) ファイル分割の原則

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

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

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

(10) 最後に

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

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


練習問題 : 問題7.

一週間で学べるコースの一覧
Udemy
...
2024/10/01

Udemyでも学びましょう!

一週間でわかるC言語・C++言語がオンライン講座になりました!動画音声によってさらにわかりやすくなりました!! 1講座で2つの言語を学ぶことができる上に、練習問題の回答もダウンロードできます。

Read →
Impress一週間シリーズ
1週間でC言語の基礎が学べる本
2024/10/01

書籍化された一週間シリーズ

本講座が「1週間でC言語の基礎が学べる本」として書籍化されました!サイトの内容プラスアルファでより学習しやすくなっています!Impressより発売中です!!

Read →
Impress一週間シリーズ
...
2024/10/01

書籍化された一週間シリーズ

一週間シリーズは書籍化されています。こちらもどうぞ!

Read →
プログラマーなら欲しいグッズ
プログラミンググッズ

プログラミンググッズ

快適なプログラミング環境を構築したい人々にぜひとも揃えてほしいグッズです。

Read →
制作・管理
シフトシステム株式会社

シフトシステム株式会社

このサイトはシフトシステム株式会社によって制作・管理がなされています。

Read →