ヘッダファイルとモジュールの作成

   さて,ヘッダファイルには何を書けばいいのでしょう。それを知るために,実験をしてみましょう。

次のソースコードを見て下さい。
これは2次元ベクトルを表す構造体と,2次元ベクトルの加算を行う vAdd( )関数を定義しています。
そして,それらを利用するmain( )関数が定義されています。
main.c

#include <stdio.h>

typedef struct vector2d {
	double x, y;
} vector2d;

vector2d vAdd( vector2d a, vector2d b ) {
	vector2d v;
	v.x = a.x + b.x; v.y = a.y + b.y;
	return v;
}

int main() {

	vector2d v1 = { 2.0, 3.0 };
	vector2d v2 = { 4.0, 1.0 };

	vector2d vResult = vAdd(v1, v2);

	printf("(%g, %g)\n", vResult.x, vResult.y);

	getchar();

	return 0;
}


これを,次の様に2つのソースファイルに分けてみましょう。
vector2d.c

#include <stdio.h>

typedef struct vector2d {
	double x, y;
} vector2d;

vector2d vAdd(vector2d a, vector2d b) {
	vector2d v;
	v.x = a.x + b.x; v.y = a.y + b.y;
	return v;
}
main.c
int main() {

	vector2d v1 = { 2.0, 3.0 };
	vector2d v2 = { 4.0, 1.0 };

	vector2d vResult = vAdd(v1, v2);

	printf("(%g, %g)\n", vResult.x, vResult.y);

	getchar();

	return 0;
}

すると,vector2d.c では特に問題は出ません(vector2d.cをプリプロセッサで処理した翻訳単位はコンパイルできる,ということ。)。
しかし,main.c では,多くのエラーが発生します。

原因は,
  ・main.c ではprintf( )関数とgectchar( )関数を使うのに,必要な stdio.h のインクルード命令がvector2d.cに移動してしまっている。
  ・vector2d型を使っているのに,コンパイラにはvector2d型がどのような型が分からずコンパイルできない(vector2d型の定義がvector2d.cに移動してしまっている)。
  ・ 関数vAdd()を使っているが,『vAdd()関数がどのような仮引数構成かわからないし,どのような値を返すか』もわからないので,
   コンパイラはvAdd()関数を呼び出している部分をコンパイルできない(vAdd()関数の定義が,vector2d.cに移動してしまっているため)。
といったところです。

では,main.cに不足しているものを補ってみましょう。上掲のvector2d.cと下のmain.cの組み合わせでコンパイルしてみて下さい。
vector2d.c

#include <stdio.h>

typedef struct vector2d {
	double x, y;
} vector2d;

vector2d vAdd(vector2d a, vector2d b) {
	vector2d v;
	v.x = a.x + b.x; v.y = a.y + b.y;
	return v;
}
main.c
#include "stdio.h>

typedef struct vector2d {
	double x, y;
} vector2d;

vector2d vAdd(vector2d a, vector2d b) {
	vector2d v;
	v.x = a.x + b.x; v.y = a.y + b.y;
	return v;
}

int main() {

	vector2d v1 = { 2.0, 3.0 };
	vector2d v2 = { 4.0, 1.0 };

	vector2d vResult = vAdd(v1, v2);

	printf("(%g, %g)\n", vResult.x, vResult.y);

	getchar();

	return 0;
}

ところが,これはリンク時にエラーになってしまいます。なぜなら,vectir2d.cとmain.cの両方にvAdd()関数の定義が書かれていて,
それぞれをコンパイルした結果,vAdd()関数の実体が2つ出来てしまい,バッティングしてしまっているためです。

そこで,main.cには,vAdd()関数の使い方(『どのような仮引数構成で,どのようなデータ型の値を返すか』)だけの手がかりを与える
関数プロトタイプ宣言のみを記述し,vAdd()関数の定義自体は書かないことにしましょう。

以下が従来のvector2d.cと改良されたmain.cです。main.cの7行目がvAdd()関数の関数プロトタイプ宣言です。
なお,vector2d.cにもvAdd()関数の関数プロトタイプ宣言が書かれていますが,エラーにはなりませんね。
このように,同じ関数のプロトタイプ宣言と定義は,同じ翻訳単位に含まれていても大丈夫なのです。

vector2d.c
#include <stdio.h>

typedef struct vector2d {
	double x, y;
} vector2d;

vector2d vAdd(vector2d a, vector2d b); /* vAdd()関数の関数プロトタイプ宣言 */
vector2d vAdd(vector2d a, vector2d b) {
	vector2d v;
	v.x = a.x + b.x; v.y = a.y + b.y;
	return v;
}
main.c
#include <stdio.h>

typedef struct vector2d {
	double x, y;
} vector2d;

vector2d vAdd(vector2d a, vector2d b); /* vAdd()関数の関数プロトタイプ宣言 (ここではvAdd()関数の外部参照宣言の役割を果たしている) */

int main() {

	vector2d v1 = { 2.0, 3.0 };
	vector2d v2 = { 4.0, 1.0 };

	vector2d vResult = vAdd(v1, v2);

	printf("(%g, %g)\n", vResult.x, vResult.y);

	getchar();

	return 0;
}

さて,これでエラーはなくなりましたが,両者の3〜7行目はまったく同じですので,vector2d.hというファイルに集約して,
vector2d.cとmain.cの両方からインクルードしてみましょう。


vector2d.h

typedef struct vector2d {
	double x, y;
} vector2d;

vector2d vAdd(vector2d a, vector2d b); /* vAdd()関数の関数プロトタイプ宣言 */

vector2d.c
#include <stdio.h>
#include "vector2d.h"

vector2d vAdd(vector2d a, vector2d b) {
	vector2d v;
	v.x = a.x + b.x; v.y = a.y + b.y;
	return v;
}
main.c
#include <stdio.h>
#include "vector2d.h"

int main() {

	vector2d v1 = { 2.0, 3.0 };
	vector2d v2 = { 4.0, 1.0 };

	vector2d vResult = vAdd(v1, v2);

	printf("(%g, %g)\n", vResult.x, vResult.y);

	getchar();

	return 0;
}


これで,無駄に同じ事を何回も書かなくて済みますね。これが,ヘッダファイルの基本的な考え方です。

※一般に『Aの宣言』とは,Aの使い方(データ型)を明らかにする記述で,コンパイルしても実行コードやデータの実体は生成されません。
※一般に『Aの定義』とは,Aの詳細を記述するもので,コンパイルされるとAの実行コードやデータの実体を生成します。『定義』は『宣言』の役目も果たします。
※変数に関しては,『宣言』と『定義』がほぼ同じ意味になる事が多いので注意して下さい。


以上の基本を踏まえて,ヘッダファイルとソースファイルに何を書く必要があるのかを整理します。

■モジュールヘッダファイルに書く内容

 ヘッダファイルから見ていきましょう。

 まず,モジュールで定義されている型・列挙定数・マクロ定義は,モジュールを利用する側でも必要になるでしょうから,それらをヘッダファイルに入れる必要があります。

 また,モジュールを利用する側がいちいち全ての外部参照宣言を自前で記述するのは大変です。そこで,モジュールのヘッダファイルに,そのモジュールが外部に公開する大域変数と関数の外部参照宣言を全て記述しておけば,そのモジュールを利用する側は,ヘッダファイルを読み込むだけでそのモジュールを正常に使用できるようになります。

 そして,モジュール自体も他のモジュールを利用しているでしょうから,それらのヘッダファイルをインクルードしていることになります。

 整理するとモジュールのヘッダファイル(.h)の内容は

   (1)他のモジュールのヘッダファイル群のインクルード  
   (2)公開しても良い型・列挙定数・マクロの定義
   (3)公開しても良い大域変数・関数の外部参照宣言

ということになります。

  このように,モジュールのヘッダファイルには,モジュールを使うために必要な情報がまとまっているので,ヘッダファイルのことを“モジュールのインタフェイス(interface)”と呼んだりします。

 さて,これらのヘッダファイルの要素をよくみると,(1)と(2)はモジュールのソースファイルでも必要なものです。しかし,いちいちヘッダファイルとソースファイルで同じ内容を書くというのもめんどうですし,それらを矛盾の無いように常に一致するようにするのは困難です。
 そこで,モジュールヘッダファイルをモジュールソースファイル側でもインクルードして,その内容を利用すればよい,ということになります。

 このとき, (3)の外部参照宣言がモジュールのソースファイルにインクルードされても問題はありません。大域変数の外部参照宣言と初期化つき宣言が同じ翻訳単位に存在する場合,外部参照宣言の方は無視されます。

 また,関数プロトタイプ宣言は,むしろソースファイルに読み込んだ方がよいのです。なぜなら,前述したようにコンパイラは未定義の関数がでてきた場合に,関数プロトタイプが無いと適当に返却値型や引数の型を予測して関数呼び出しの部分をコンパイルしてしまいます。互いに呼ばれる関数など,かならずしも関数呼び出しの前に関数の定義を行えるとは限りません。しかし,関数プロトタイプがヘッダファイルの一部としてソースファイルの先頭で読み込まれれば,
関数定義の順番に関係なく,正しく関数の呼び出しをコンパイルできるわけです。


■モジュールソースファイルに書く内容
 

 一方,モジュールのソースファイル(.c)は,

   (1)モジュールの利用者に公開したくない内容を含んだヘッ ダファイル
        (“プライベートヘッダファイル(private header file)”などと 呼ばれる)のインクルード,
        このモジュールのモジュールヘッダファイルのインクルード
   (2)公開したくない型・列挙定数・マクロの定義,
   (3)公開したくない関数のプロトタイプ宣言
   (4)公開しない関数・大域変数のstaticつき定義
   (5)公開する関数・大域変数のstatic無し定義


などを行うことになります。





 では,モジュールの単純な作例を見てみましょう。

題材は2次元ベクトルに関する演算をモジュール化して,そのヘッダファイルとソースファイルを作るというものです。
List 0.2(vector2d.h)がヘッダファイル,List 0.3(vector2d.c)がソースファイルです。

List 0.2では,ヘッダファイルが
  #if !defined( _VECTOR2D_H_ )
  #define _VECTOR2D_H_
   … 中略 …
  #endif
 という形になっていますが,これはヘッダファイルの2重読み込みを防止する工夫です。ひとつの翻訳単位でこのヘッダファイルが最初に読み込まれたときは,
ヘッダファイル全体が有効になりますが,2回目以降はマクロ_VECTOR_H_が定義されるため,ヘッダファイルの内容は読み飛ばされます。

このヘッダファイルには,

 ・2次元ベクトルを表す構造体型vector2dの定義,
 ・単位ベクトル(大域のconst定数)の外部参照宣言,
 ・ベクトル演算を行う各関数の関数プロトタイプ宣言

が記述されています。

List 02

/***
 *** vector2d.h
 ***  - 2次元ベクトル演算モジュールヘッダファイル -
 ***/

#if !defined( _VECTOR2D_H_ )
#define _VECTOR2D_H_

/* 2次元ベクトルの型定義 */
typedef struct vector2d {
  double x, y;
} vector2d;

/* 外部参照宣言 */
extern const vector2d gXUnitVector; /* x方向単位ベクトル */
extern const vector2d gYUnitVector; /* y方向単位ベクトル */

/* 関数プロトタイプ */
vector2d VAdd( vector2d a, vector2d b );      /* 加算 */
vector2d VSub( vector2d a, vector2d b );      /* 減算 */
double InnerProduct( vector2d a, vector2d b );/* 内積 */
double Norm( vector2d a );        /* ベクトルの大きさ */

#endif /* _VECTOR2D_H_ */

 モジュールソースファイルであるList 0.3では,冒頭でmath.h(標準数学演算ライブラリのヘッダファイル)とモジュールヘッダファイルvector2d.hを読み込み,
単位ベクトル(大域のconst定数)の定義,各種演算の関数定義が行われています。

関数Norm()は平方根を求める標準数学ライブラリの関数です。このベクトル演算モジュールを利用するときには,vector2d.hを読み込んで,その機能を使うことになります。

List 03
/***
 *** vector2.c
 ***  - 2次元ベクトル演算モジュールソースファイル -
 ***/
#include <math.h>
#include "vector2d.h"

/* 単位ベクトル (大域のconst定数)の定義 */
const vector2d gXUnitVector = { 1, 0 };
const vector2d gYUnitVector = { 0, 1 };

/* 公開する関数のstatic無し定義 */
vector2d VAdd( vector2d a, vector2d b ) { /* 加算 */
  vector2d v;
  v.x = a.x + b.x; v.y = a.y + b.y;
  return v;
}

vector2d VSub( vector2d a, vector2d b ) { /* 減算 */
  vector2d v;
  v.x = a.x - b.x; v.y = a.y - b.y;
  return v;
}

double InnerProduct( vector2d a, vector2d b ) { /* 内積 */
  return (a.x * b.x) + (a.y * b.y);
}

double Norm( vector2d a ) { /* ベクトルの大きさ */
  return sqrt( InnerProduct( a, a ) );
}

■モジュール化のまとめ

下図に,マニュアルを兼ねたモジュール化のまとめを示す。


■C言語モジュール化課題
次のソースファイルは,2次元図形を扱う型・定数・関数を定義して利用している例である。
ソースファイル内の指示に従って,モジュール化せよ。

提出は
 ・2次元図形モジュールを構成するヘッダファイル fig2d.h と ソースファイル fig2d.cpp)
 ・上記のモジュールを利用している main関数を含む main.cpp
の3ファイルを zip ファイルに圧縮し,WebClassのこちらに提出すること。

※ちゃんとプログラムとして動作する様にすること。

◆正解例はこちら


main.cpp (←こちらのリンクからダウンロード出来ます)
#include <stdio.h>

// 2次元の点を表す構造体
typedef struct Point {
  double x; // x座標
  double y; // y座標
} Point;

// 2次元の円を表す構造体
typedef struct Circle {
  Point  center;
  double r;
} Circle;

// 2次元の矩形を表す構造体
typedef struct Rectangle {
  Point  leftTop;     // 左上の頂点
  Point  rightBottom; // 右下の頂点
} Rectangle;

// 原点中心の単位円(半径1.0)を表す大域定数
const Circle unitCircle = { {0.0, 0.0}, 1.0 };

// 円周率を表す定数
#define PI (3.14159265359)

// 円の面積を計算して返す関数
double calcCircleArea( const Circle c ) {
  return PI * c.r * c.r;
}

// 矩形の面積を計算して返す関数
double calcRectangleArea( const Rectangle r ) {
  return (r.rightBottom.x - r.leftTop.x) * (r.rightBottom.y - r.leftTop.y);
}

/*
ここから上を
 ・モジュールヘッダファイル fig2d.h (インクルードガードを施すこと)
 ・モジュールソースファイル fig2d.cpp
  として別のファイルにまとめ直し、ここで stdio.h と fig2d.h をインクルードせよ。
*/

int main() {

  // 底面が半径3.0の円で高さ10.0の円柱がある。この円柱の体積を求めよ。
  Circle c = { {0.0, 0.0}, 3.0 };
  printf("円柱の面積は%g\n", calcCircleArea( c ) * 10.0 );

  // 高さ6.0の直方体がある。底面の矩形は左上頂点が(0.0, 2.3), 右下頂点が(5.0, 4.0)
  // で2次元平面上にある。この直方体の体積を求めよ。
  Rectangle r = { { 0.0, 2.3 }, { 5.0, 4.0 } };
  printf("直方体の面積は%g\n", calcRectangleArea(r) * 6.0);

  // 単位円の面積を求めよ。
  printf("単位円の面積は%g\n", calcCircleArea(unitCircle));

  getchar();
  return 0;
}