おさえておきたいプログラミングの基本
ここまで紹介してきたプログラムは、どれも一つの.cファイルに記述されていました。しかし、実際の実用的なプログラムはより大規模になり、一つのファイルには収まりません。なぜならソフトウェア開発の現場には多くのプログラマーが協力してプログラムを作っているのがほとんどだからです。
そういった場合、必要になってくるのが、ファイル分割です。C言語に限らず実用的なソフトウェアのプログラムは、複数のファイルに分割されています。分割の仕方はそのプログラムの機能などによって様々です。
すでの述べたとおり、cには、拡張子が「.h」のヘッダファイルと、拡張子が「.c」のソースファイルがあります。通常、大きなプログラムは、多数の関数から成っています。そのため、それらのファイルは、規模に応じて複数に分割されることになりますが、ファイルが分割されれば他のファイルにある関数を呼び出すことが出来なくなります。
そこで、ヘッダファイルに、.cファイルに記述されているファイルの内容を記述しておき、それを読み込むことにより、他の.cファイルに記述されている関数を利用することが出来るようになります。実は、このヘッダファイル、プロトタイプ宣言が記述されているファイルなのです。
では、実際のプログラムでファイル分割を試してみましょう。まずは、関数のところで説明した、list6-3を使ってみましょう。
list6-3:main.c 【再掲載】#include <stdio.h>
// 関数avgのプロトタイプ宣言
double avg(double,double);
int main(int argc, char** argv) {
double d1,d2,d3;
double a = 1.2,b = 3.4,c = 2.7;
// 同じ計算が3回(関数を呼び出して計算)
d1 = avg(a,b);
d2 = avg(4.1,5.7);
d3 = avg(c,2.8);
printf("d1 = %lf,d2 = %lf,d3 = %lf\n",d1,d2,d3);
return 0;
}
// 平均値を求める関数
double avg(double m,double n){
// 引数l,mの平均値を求め、rに代入する。
double r = (m + n) / 2.0;
return r;
}
このプログラムは、一つの関数から成り立つプログラムです。これをファイル分割にすると、以下のようになります。
list7-1①:calc.h#ifndef _CALC_H_
#define _CALC_H_
// 関数avgのプロトタイプ宣言
double avg(double,double);
#endif // _CALC_H_
list7-1②:calc.c
#include "calc.h"
// 平均値を求める関数
double avg(double l,double m){
// 引数l,mの平均値を求め、rに代入する。
double r = (l + m) / 2.0;
return r;
}
list7-1③:main.c
#include <stdio.h>
#include "calc.h"
int main(int argc, char** argv) {
double d1,d2,d3;
double a = 1.2,b = 3.4,c = 2.7;
// 同じ計算が3回(関数を呼び出して計算)
d1 = avg(a,b);
d2 = avg(4.1,5.7);
d3 = avg(c,2.8);
printf("d1 = %lf,d2 = %lf,d3 = %lf\n",d1,d2,d3);
return 0;
}
では、list7-1を用いて、ファイル分割の基本について説明してみましょう。このサンプルでは、「calc.h」というヘッダファイルを、main.c、calc.cでインクルードしています。
calc.hのインクルードもともと、#includeは指定したヘッダファイルを読み込むために利用します。この処理は、calc.hの中のプロトタイプ宣言を利用する必要があるのです。main.cでは、calc.hの中でプロトタイプ宣言をされている関数を利用するためにこの処理を記述しています。
それに対し、calc.hのプロトタイプ宣言の定義を行うソースファイルとしてcalc.cを作っています。このような場合も、calc.hを読み込む必要があります。
次にlist7-1①の、ヘッダファイルcalc.hをみてみましょう。一般に、ヘッダファイルの書式は以下のようになります。
基本的なヘッダファイルの書式冒頭に出ている#ifndef、#define、#endifは、マクロと言い、C言語そのものの文法とは無関係ですが、コンパイラに指令を与えるものです。詳細はここでは省略しますが、これにより、二重インクルードを防いでいます。
二重インクルードとは、一つのヘッダファイルが複数箇所で箇所で読み込まれることにより、その中で記述されている関数のプロトタイプ宣言などが重複されて実行されてしまう現象です。この時、エラーが発生してコンパイルをすることはできません。
では一体、なぜこのような処理で二重インクルードが防止できるのでしょうか?そのためには、各マクロの意味を理解する必要があります。
defineマクロはこのように定数などを定義するときに便利なため、二重インクルードを防止する以外でも利用されます。
まずはマクロの中で一番簡単で使用頻度が高い#defineマクロから説明しましょう。#defineマクロはテキスト置換を行うために使用されます。書式は次の通りです。
#defineマクロの書式マクロ名は定義したいマクロの名前であり、置換テキストはマクロが置換される内容です。
#defineマクロはプログラム内の任意の場所で使用することができます。マクロはプログラムのソースコードを解析する前に置換され、その結果としてプログラムの本体にマクロの置換テキストが挿入されます。例えば次のように使用します。
#defineマクロの使用例このように定義することにより、プログラムの中に「PI」という文字列が現れると、「3.14159」 に置き換えられます。
次に#endifで終わるマクロを説明します。#if、#ifdef、#ifndefは、いずれも#endifで終わるマクロであり、複数行で使用します。それぞれ次のような意味があります。(図7-1)
図7-1.#endifで終わるマクロの一覧とその意味(1) |
|
|
---|---|---|
(2) |
|
|
(3) |
|
以上を踏まえ、あらためて二重インクルード防止の仕組みを見てみましょう。普通ヘッダファイル、複数のファイルで参照されます。list7-1でも、calc.c、calc.hでインクルードされます。(図7-2)
図7-2.ヘッダファイルとソースファイルの関係そのため、もし二重インクルード防止処理がなされていなければ、1回目は良いのですが、2回目のインクルードでこの間で定義されているプロトタイプ宣言が二回定義されることになり、コンパイルエラーになります。
このとき、#ifndef~#endifマクロで、二重インクルードの防止がなされていなければ、関数などが二重に定義されてしまいエラーになりますが、これではさむことにより、一度定義されたものは二度定義されることは無いのでエラーになりません。
その理由は、1回目のインクルードではまだ_CALC_H_が#defineで設定されていないため、#defineから#endifまでの間が読み込まれます。しかし、2回目のインクルードでは、すでに_CALC_H_が#defineで設定されているため、二重インクルードを防止したい部分は読み込まれないからです。(図7-3)
図7-3.二重インクルードの防止処理なお、#defineの後の部分は、calc.hの場合には_CALC_H_といったようにヘッダファイルの名前に由来したキーワードを用いる慣例になっています。絶対にそうでなくてはならないという文法的規約ではありませんが、この方法が大変わかりやすいため、一般的に用いられている方法です。
ところで、1日目から特に説明をしてきませんでしたが、C言語のプログラムでは冒頭で必ず以下の処理をしていました。
C言語の冒頭で行われる#includeの処理この処理はC言語の標準ライブラリの一つである、標準入出力の関数のプロトタイプが宣言されているヘッダファイルであえるstdio.hを読み込むものだったのです。
calc.hというヘッダファイルを読み込む場合、#include "calc.h"と記述し、ヘッダファイルのファイル名は"(ダブルクオーテーション)で囲っています。しかし、では、この記述と#include <stdio.h>の場合は不等号で囲っています。一体この違いはなんなのでしょうか?
通常.hファイルに対しその関数を記述した部分が、.cファイルにソースコードとして書かれている場合は、.hファイルの読み込みは、ダブルクオーテーションで行います。それに対し、関数の定義の部分が、ライブラリファイル(.lib)など、すでにコンパイルされているものに関しては、<と、>でヘッダファイル名を囲みます。(図7-4)
図7-4.ヘッダファイルとソースファイルの関係次に分割するファイルを少し複雑にし、互いに依存関係のある複数のヘッダファイルに分割する場合を考えてみましょう。まずは、以下のサンプルを見てください。
list7-2:main.c#include <stdio.h>
// 計算の答え(グローバル変数)
int ans = 0;
void add(int,int);
void sub(int,int);
void showAnswer();
int main(int argc, char** argv) {
int a = 2,b = 3;
printf("%d + %d = ",a,b);
add(a,b);
showAnswer();
printf("%d - %d = ",a,b);
sub(a,b);
showAnswer();
return 0;
}
void add(int a,int b){
ans = a + b;
}
void sub(int a,int b){
ans = a - b;
}
void showAnswer(){
printf("%d¥n",ans);
}
実行結果
このプログラムは見ても判る通り、単純な加算・減算を行うプログラムです。少し変わっているところといえば、その結果が、ansというグローバル変数に入っているところでしょう。
list7-2をファイル分割してみます。その際、機能に応じて計算部分をcalc.h/.cというファイルに、結果表示部分をshowResult.h/.cというファイルにそれぞれ分割してみることにします。
list7-3①:main.c#include <stdio.h>
#include "calc.h"
#include "showResult.h"
int main(int argc, char** argv) {
int a = 2,b = 3;
printf("%d + %d = ",a,b);
add(a,b);
showAnswer();
printf("%d - %d = ",a,b);
sub(a,b);
showAnswer();
return 0;
}
list7-3②:calc.h
#ifndef _CALC_H_
#define _CALC_H_
void add(int,int);
void sub(int,int);
#endif // _CALC_H_
list7-3③:showResult.h
#ifndef _SHOW_RESULT_H_
#define _SHOW_RESULT_H_
void showAnswer();
#endif // _SHOW_RESULT_H_
list7-3④:cacl.c
#include "calc.h"
int ans;
void add(int a,int b){
ans = a + b;
}
void sub(int a,int b){
ans = a - b;
}
list7-3⑤:showResult.c
#include "showResult.h"
#include <stdio.h>
extern int ans;
void showAnswer(){
printf("%d¥n",ans);
}
ヘッダファイルが、calc.h、showResult.hの2つに別れ、それぞれのヘッダファイルに対応する実装が、calc.c、並びにshowResult.cに記述されているのがわかると思います。
ただ、問題は、グローバル変数ansの対応方法です。すでに述べたように、グローバル変数は、プログラム全体で利用できる変数です(6日目参照)。しかし、この例のように、プログラムが複数に分割された場合、宣言されているファイル以外の場所では、グローバル変数は使えなくなってしまいます。
この変数は、calc.c、showResult.cの両方で使用するのですが、定義はどちらか一箇所にしか出来ません。このようなとき、活躍するのが、extern(エクスターン)修飾子です。
extern修飾子の使用例externは、英語で「外に」を意味を持つ言葉です。つまり、この例では、「int ans;」という宣言が他のファイルにあることを意味します。実際、見てみると、calc.cの3行目に、「int ans」があることがわかります。
つまり、externを付けることにより、その変数の宣言が用いられるファイルの外側にある定義、つまり、この例だとcalc.cの中のint ansを用いることになるのです。
以上から、cac.cの中のadd()およびsub()関数で用いられているansと、showResult()関数の中で用いられているansは、同じものをさしているのです。
よって、異なるソースファイルの中で、共通のグローバル変数を利用しているということになるのです。(図7-5)
図7-5.externの使い方ここで取り上げたファイル分割は、あくまでも初歩のものです。enumが入ったり、データの隠ぺいが必要な場合など、より高度なファイル分割については、応用編第7日目を参考にしてください。
では最後に、いままで内容を踏まえて、こういった複数に分割されたヘッダファイル・ソースファイルがどのようにしてマシン語に変換されているのかを理解するため、C言語のコンパイラの仕組みをより詳しく説明しておくことにしましょう。まずは、以下の図を見て下さい。(図7-6)
図7-6.Cコンパイラの仕組みすでに述べた通り(0日目参照)、C言語のプログラムは、コンパイラによって、最後にはマシン語に変換されて、実行されます。変換されたマシン語は、実行ファイルと呼ばれるファイルに記録されます。
Cコンパイラの仕事は、ソースファイルを最終的に実行ファイルに変換することなのです。この処理には大きく分けて、以下の3つのプロセスがあります。
①プリプロセッサ
プリプロセッサは、ソースコードに一定の規則に従って処理を加えます。これによって、各ソースファイルおよび #includeや、#defineといったような、マクロの処理をするのが段階です。一般に、こういった命令をディレクティブと呼びます。この段階で行われるのは、いわばコンパイルの前処理といったところです。
②コンパイラ
プリプロセッサで処理されたコードを機械語に翻訳するのが、コンパイラの役割です。ただ、ここでは実行可能な形でのファイル ではなく、obj(オブジェ)ファイルもしくは、オブジェクトコードと呼ばれるファイルが形成されます。
オブジェクトファイルは、機械語に変換されたコードの断片の集まりであり、これらが最終的につながる(リンク)されることにより、 実行可能なファイルになります。
③リンカ
最終的に、コンパイラで作成された複数のオブジェクトファイルを一つにまとめて、実行ファイルを作るのがこの段階です。ただ、 Cの標準ライブラリなど、Cのソースコードだけでは足りない部分は、lib(ライブラリ)ファイルとして、ここで追加されます。 これらが統合されて、最終的な実行ファイルになります。
OSがウィンドウズの場合、実行ファイルには、「exe」という拡張子がついています。このファイルを、そのつづりから「エグゼファイル」などと 呼んだりします。
以上がCコンパイラの仕組みです。このように、プリプロセッサから、リンカまでの処理を通して、一般に、ビルドという言い方をします。 VisualStudioや、Eclipseなどの統合開発環境(とうごうかいはつかんきょう)は、ソースコードの入力から、ビルド、更には実行までを一手に引き受けてくれる プログラムなのです。
以上で、C言語の基本は終了です。しかし残念ながらこれまでの知識だけでは十分なプログラムが作れるとは言えません。しかし、ここにはC言語を学ぶ初心者が最初に抑えておくべき基本事項が網羅されています。あとは、この応用に過ぎません。ですので、これまでの内容をしっかりと学習しより高いレベルにチャレンジしてみてください。
より高いレベルにチャレンジしたい学習者のために、応用編が用意されています。ここまでの内容を理解した方は、ぜひチャレンジしてみてください。
→ 応用編第1日目へ
また、これまでの知識をベースにさらなる専門知識を身につけるために、さまざまな書籍を利用して学習してみるとよいでしょう。
練習問題 : 問題7.
一週間でわかるC言語・C++言語がオンライン講座になりました!動画と音声によってさらにわかりやすくなりました!! 1講座で2つの言語を学ぶことができる上に、練習問題の回答もダウンロードできます。
Read →本講座が「1週間でC言語の基礎が学べる本」として書籍化されました!サイトの内容プラスアルファでより学習しやすくなっています!Impressより発売中です!!
Read →