このエントリーをはてなブックマークに追加
スポンサーリンク

C言語入門

はじめに

C言語は、BASICやCOBOLのような高レベル言語と、アセンブラのような機械語との中間のような言語です。つまり、BASICのようなインタプリタ言語ほど人間に近い言葉ではないにせよ、アセンブラほど機械語にも近くない言語といえます。

つまり、簡単に言えば、低レベル制御のできるコンパイラ言語という事になります。

MS-DOS(16ビット)時代では、装置ドライバなどはアセンブラで記述するのが主でしたが、32ビット・64ビット時代になると速度を要求する処理にアセンブラが使われることは稀で、ほとんどが低レベル制御が可能なC言語で記述されるようになりました。このように、C言語は今やプログラム開発にはかかせないものとなりました。

C言語の特徴として、
  • BASIC言語では『サブルーチン』と呼ばれていたものを、『関数』という概念で扱う。
  • 関数を呼ぶための関数(もしくは、それを呼ぶための関数)等々、関数を拡張してゆく事ができる。
  • 構造化プログラミングの概念をもちつつ、goto文も使えるという、柔軟な設計が可能である。
といった所でしょう。入門編では、C言語で重要と思われる、文法、型の宣言、printf、ポインタ、ループ・条件判断、演算子、ファイルについて見てゆきたいと思います。

文法


C言語では、基本的に改行コードにあまり意味がありません。(プリプロセッサは除く)1つの行は;(セミコロン)で区切ります。1行が長くなる場合には途中で改行コードを入れてもかまいません。(変数や関数の途中ではダメですが) とにかく、コンパイラはセミコロン〜セミコロンが1行とみなします。

例)
printf("hello word\n");

コメント
コメント文は、/*  */で囲まれた部分がコメントとなり、この間に書かれたものはコンパイルされません。他の言語の多くが改行コードがコメント文の終わりであるのに対し、C言語では途中に改行を入れてもコメントの終わりとはならず、*/が来るまではコメントとみなします。

例)
/*  この行はコメントです  */

関数
関数とは、ある引数(複数になる場合も)を与えると、引数を使って決まった計算をし、結果を返す処理の事です。C言語には『メインルーチン』『サブルーチン』なる概念がありません。すべてが『関数』という概念になっています。ちなみに、他の言語で『メインルーチン』と呼ばれるものは、C言語では、main()と呼ばれる1つの関数です。コンパイラはこのmain()関数をまず初めに実行し、main()関数の終了が、プログラムの終了という事になります。

C言語では、関数をいくつも組み合わせることで1つのプログラムになります。

関数は、
戻り値の型 名前(引数の型 引数名) { 
     :
   関数本体
      :
      :
      :

   return 戻り値;
のように記述します。例えば、
int nibai(int a) {
  int b;
  b=a*2;
  return b;
}
の場合、戻り値がint型、名前がnibai、引数がint型のaで、関数本体が、{と}で囲まれた部分という事になります。この場合、引数を2倍した結果を返す関数で、 c=nibai(2)とか書くと、引数(この場合は2)が2倍され、cに4が入るわけです。

void
C言語では、頻繁にvoidという言葉が使われます。これは、簡単にいうと『ない』という意味です。たとえば、 void main() のように、関数の前にvoidをつける事で、『この関数には戻り値がありません』という意味になります。

同様に、 int kansuu(void)と書くことで、『この関数には引数がありません』という意味になります。

大文字小文字
C言語では、命令はすべて小文字で書きます。また、変数名の大文字小文字は区別します。hensuuとHENSUUは別の変数になります。

変数名は基本的には全てを小文字で記述しますが、FILEなどの構造体のタグは大文字、defineで定義された定数は大文字で表記し、C++で使われるクラスでは頭文字だけを大文字にような習慣があります。また、defineを使わなくとも、定数としてとしての意味が強い変数(つまり、プログラムの途中で頻繁に代入したりしない)は、全てを大文字で書く習慣があります。

そういう規則で作らなくともコンパイルエラーが出るわけではないし、強制ではないのですが、既にこの規則が一般化しているので、これからプログラムを学習する人はその風習に従うことにしましょう。

型の宣言

C言語では、変数は宣言してから使います。宣言する時に、型を指定します。
長さ
int 一度にpushできる長さ
char 8ビット
short intと同じ場合が多い
long intの2倍のビットである事が多い
long long intの4倍のビットである事が多い
float 単精度浮動小数点演算
double 倍精度浮動小数点演算
ここで、明確に長さが決まってるのはcharだけです。charは8ビット(1バイト)と決まっており、扱う値が1バイト以内で十分(-128〜127)な場合に使います。ポインター変数では、1バイト単位である事を示します。文字列はchar*で定義する場合が多いです。

intは基本的には「そのコンピューターが一度にPUSHできるビット数」となっていますが、64ビット機でMS-DOSを動かしても16ビットですし、32ビット版のOSを動かすと32ビットですし、実際にオーバーフローさせて確かめてみるしかなさそうです。
16ビットシステム(MS-DOS等) 32ビットシステム(Linux i386等)
長さ 扱える数 長さ 扱える数
(signed) int 16ビット -32768〜32767 32ビット -2147483648〜2147483647
unsigned int 16ビット 0〜65535 32ビット 0〜4294967295
(signed) char 8ビット -128〜127 8ビット -128〜127
unsigned char 8ビット 0〜255 8ビット 0〜255
(signed) short 16ビット -32768〜32767 32ビット -2147483648〜2147483647
unsigned short 16ビット -32768〜32767 32ビット 0〜4294967295
(signed) long 32ビット -2147483648〜2147483647 64ビット -9223372036854775808〜9223372036854775807
unsigned long 32ビット 0〜4294967295 64ビット 0〜18446744073709551615
float 32ビット 有効数字6桁の浮動小数点 64ビット 有効数字15桁の浮動小数点
doubole 64ビット 有効数字15桁の浮動小数点 64ビット 有効数字15桁の浮動小数点
MS-DOSの時代では、intが16ビット、longが32ビットでしたが、64ビットで計算したくともlong longと定義してもlongと同じ32ビットになってしまうコンパイラばっかりでした。なので10桁以上の数値はdouble型で定義したりしてました。

Linuxではfloatであっても64ビットで計算してくれるコンパイラが多くなりました。(精度を指定した場合は別ですが)コンパイラによっては互換性のためにfloatと明確に指定すると32ビットまでわざと精度を落とすために、かえって低速になってしまう場合もあるようですが、大抵はLinuxで小数点を計算したい時はfloatと指定すれば倍制度で計算してくれるようです。

変数の有効範囲

変数には、ローカル変数と、グローバル変数があります。ローカル変数とは、関数の中で宣言された変数の事で、その宣言された関数の中でのみ使用できます。また、グローバル変数とは、関数の外で宣言され、その実行単位すべてで共通して使用可能です。
例)
int ghensuu;

main(){
  int lhensuu;
}

main()の外で宣言されたghensuuはグローバル変数で、main()内で指定されたlhensuuはローカル変数になる。lhensuuはmain()内でのみ使用可能。
グローバル変数はあまり使わない方が良いとされていますが、全関数で共通して使う定数や、そのシステム内で共通で使うバッファへのポインタなどはグローバルで定義しておくと、なにかと便利です。

この他に、宣言する際に、記憶クラスを指定する事ができます。
変数の種類 記憶クラス 意味
グローバル変数 static コンパイル時に一度だけ初期化される。
extern 他のコンパイル単位にある変数。
ローカル変数 static コンパイル時に一度だけ初期化される。
register 宣言された実行単位に移る毎に初期化される。
auto
staticはデーター記憶領域にメモリを割り当て、コンパイル時に一度だけ初期化されます。グローバル変数で記憶クラスを省略すると、staticが宣言されたものみなします。

externは分割コンパイル時に、他の実行単位にその変数の宣言がある事を示し、実体はそこでは確保されません。リンク時に、どこにもexternで宣言した変数の実体がないと、エラーになります。

autoは、記憶領域としてスタックを使用し、その実行単位に制御が移る毎に初期化されます。たとえば、kansuu1();内で宣言されたローカル変数は、その関数が呼ばれるたびに初期化されます。ローカル変数で記憶クラスを省略した場合、自動的にautoが指定された事になります。その関数が終了した地点でスタックは破棄され、ローカル変数の値も破棄されます。

registerは、指定されたローカル変数を、レジスターで保存する事を示しています。これにより、高速化をはかることができます。ただし、registerで宣言した変数が多すぎると、レジスターを割り当てることができなくなるため、スタックが使用されるようになります。(autoと同じになる) 最近では、機械の性能も向上しているため、あまりスピードにこだわる必要がなく、registerはどうしてもという場合以外はあまり使わないと思います。

printfを使ってみる

では、さっそく、printf文を使って画面に何か文字を表示させてみましょう。

#include <stdio.h>
main() {
  printf("hello word\n");
}

これは、stdio.h内で宣言されたprintfという関数を使って、hello wordという文を出力するための関数(mainという名の関数)です。printfは、『決められた書式で文字列を標準出力に出力せよ』という関数です。

ここで、まず注目して欲しいのは、printfのあとの()です。この()の中身を引数といいます。この引数を関数に渡し、関数ではそれを処理します。printfの場合、まず、”…”(ダブルクォートで囲まれた部分)を処理します。ここに通常の文字があればそのまま表示し、指示子があれば、その規則にしたがって処理をします。

この””の中にある¥nに注意してください。これは、別に¥nという文字が出るのではなく、制御文字といって、特別な意味があります。
制御文字 意味
¥n 改行と復帰
¥t タブ
¥b バックスペース
¥r 復帰
この¥をエスケープキャラクタと呼び、この記号に続く次の文字と組み合わせて、1つの意味を持ちます。また、¥マーク自身を表記したい時は、¥¥という風に、エンマークを2個続けて書きます。この場合、hello word\nと書かれていますので、画面に『hello word』と表示した後、改行と復帰を行うという事です。(つまり、標準出力に0AHを出力させるわけです。)

次に、printf中に数値を表示したい場合を考えてみましょう。この場合、指示子を入れます。これは、%に続いて、記述します。

[%][右詰め左詰め区分][ゼロで埋めるかどうか][整数の桁][.][少数の桁][型]

右詰め左詰め区分
左詰め
右詰め
ゼロでうめるかどうか
ゼロで埋める
指定なし ゼロで埋めない
整数の桁、小数の桁は、それぞれ数字で表します。

int型を10進数で表す
int型を16進数で表す
int型を8進数で表す
符号なしで表す
文字列を表す
キャラクターを表す
指数形式で表す
実数形式で表す
e、fのうち短い方で表す
たとえば、
%-5.0d
と書いた場合、左詰めの、整数部5桁、小数部0桁、int型の数値を10進数で出すという意味になります。

ここで、もしunsignedで宣言してある数値だとしても、型にdを指定すると、マイナスで表記されてしまう場合があります。uneignedで宣言した数値は、uを指定するようにします。また、longを指定した場合、luという風に、先頭に小文字のl(エル)を書いてください。また、double longの時は、fの前に大文字のLをつけてください。

このように、printf内では、宣言したときの変数の型にあったものを、このprintf内で指定します。宣言したときの型をprintfが自動的に判断してくれるわけではないので、注意してください。

この、ダブルクォートで囲まれた部分を記述した後、,(カンマ)で区切って表示したい変数を並べて書きます。

例)

int a=10;
printf("a=%d\n",a);

の場合、結果は、a=10と表示されます。

配列

C言語でいう配列は、BASICやCOBOLでの配列とは少し意味あいが異なります。C言語でいう配列は、『ポインター変数を宣言し、決められたバイト数分だけ実体を確保する』という事です。

また、参照する方法が、ポインター変数とは異なっています。

宣言
たとえば、文字列を格納する配列は、

char moji[80];
みたいに宣言します。また、宣言と同時に、
static char moji[80]="MOJI"
みたいに初期化する事ができます。

また、数値配列だった場合、
int suuji[10];
みたいに宣言します。宣言と同時に初期化したい時は、
static int suuji[10]={1,2,3,4,5,6,7,8,9,10};
みたいにします。

代入
配列に値を代入する場合、
moji[0]='M';
moji[1]='O';
moji[2]='J';
moji[3]='I';

みたいに、1個ずつしか代入できません。そのため、通常はいったんポインター変数に値をいれておき、strcpyなどでコピーします。

添字
BASICやCOBOLでは添字にゼロを入れるとエラーになるか、あるいは1が指定されたものと見なされますが、C言語では添字は、0から始まります。したがって、配列を80個定義した場合、実際には添字は0〜79になります。

なお、添字をオーバーした場合、BASICやCOBOLではエラーになりますが、C言語ではエラーにはならず、どこかのメモリを破壊するだけなので、注意が必要です。MS-DOSでは暴走、Linuxではセグメンテーションフォルトになると思います。

ポインタ変数

ポインタ変数とは、つまり、「変数を格納した場所を示した変数」という事になります。

たとえば、
int *a;

とすると、変数aの実体は確保されませんで、変数aをの示すポインターが確保されたという事です。実際には、ポインター変数を初期化するか、あるいは、malloc( )で確保するまで実体がありません。つまり、

int *a;
gets(a);

などとすると、メモリーを破壊してセグメンテーションフォルトとか、予期せぬ動作をします。というのも、実体のないポインター変数aにキーボードから入れた文字列を入れてしまうからです。この時、aはどこを指しているかわかりませんので、とにかく『どっかが壊れる』事になります。

そこで、gets()などを使う場合、mallocを使って、実体を確保しておく必要があります。
例)

char *keyin;

keyin=malloc(80);
keyinというポインタ変数に、80バイト確保する。
なお、メモリが足りなくて確保できない場合のことも考えて、通常は以下のように作ります。
if (NULL == (keyin=malloc(80))) {
   printf("メモリが足りません\n");
   return;


メモリが確保できない場合、『メモリが足りません』というメッセージを表示し、関数を抜ける。

値渡しとポインタ渡し

C言語では、関数から関数を呼ぶ場合、引数として値そのもの、もしくは、ポインターを渡します。

値渡し
値そのものを渡す場合、渡したい値がスタックにプッシュされ、呼び出された先でポップされて使われます。そのため、呼び出された側で値を変更しても、呼び出し側には影響を受けません。
例)

nibai(int x){
 x=x<<1
 return (x);
}

main(){
 int a,b;
 a=1;
 b=nibai(a);
 printf("a=%d, b=%d\n",a,b);
}

関数nibaiは、引数で指定された値を二倍にして返す。ここでは、a=1であるから、結果、b=2となる。
ここで、aは値渡しなので、関数を呼んでもaの値は変化しないので、a=1のままになる。
ポインター渡し
ポインター渡しの場合、その変数のアドレスがスタックにプッシュされ、呼び出され側でポップされます。そのため、呼び出した側と、呼び出された側では、同じアドレスのメモリーを参照する事になるため、呼び出され側で変数を変更すると、呼び出し側の方でも変数が変化します。
例)

void nibai(int *x){
  *x=*x<<1
}

main(){
  int a;
  a=1;
  nibai(&a);
  printf("a=%d\n",a);
}

関数nibaiは、引数で指定された場所にあるメモリを二倍にする。この場合、ローカル変数aが格納されているアドレスを渡し、呼び出され側の方で、ローカル変数aが格納されている場所にある値を*xとし、それを直接二倍にしているため、関数呼び出し後にmain()の中で見るとaが二倍になっている。
この他、C++では『参照渡し』がありますが、それについてはC++の方に書きます。

ループ・条件判断

どの言語でも条件判断は必要ですが、C言語では、他の多くの言語と同様、if文が用意されています。また、ループ処理として、whileやfor文が用意されています。

if
式の値が真なら、続く{ }内の文を実行します。偽なら、else{ }内の文を実行します。
例)
if (式){
  (文1)
} else {
  (文2)


(式)が真なら、(文1)を実行し、偽なら(文2)を実行する。
while
式の値が真の間ループします。
例)

while (式){
  (文)


(式)が真の間、(文)を実行する。
do while
whileとほとんど同じですが、条件にかかわらず少なくとも1回は実行します。
例)

do{
 (文)
}while(式);

(式)が真の間(文)を実行するが、(文)は少なくとも1回は実行される。
switch〜case
(式)の値を評価し、caseで示された値の所を実行します。
例)

switch (式){
   case 1:(文1);break;
   case 2:(文2);break;
   case 3:(文3);break;
   default:(文4);


(式)の値が1なら(文1)、2なら(文2)、3なら(文3)、それ以外なら(文4)を実行する。
switchでは次のラベル(case)が来ると実行を止めるわけではなく、break;が来てswitch文から抜けます。なので、break;を忘れると、その下に記述した条件全てが実行されてしまいます。

for
ループカウンターを使ってループします。
例)

for (初期設定; 条件; 付加実行式){
  (文1)


最初に初期設定を行い、条件が真である間ループする。1回ループするごとに、付加実行式を実行する。
break
現在実行中の一番内側のループから脱出します。if文などの条件式で使う場合がほとんどです。

continue
現在実行中の一番内側のループ処理の頭に戻ります。 ループからは抜けませんが、付加実行式は実行されるため、そこで条件が偽になった場合は結果的にループから抜けます。これも条件式で使います。

分岐

C言語では、分岐命令としてgoto文が用意されています。
例)

if (式){
  goto dskerr;


  (文1)
    :
  (文99)

dskerr:
  (文100)
if (条件){
  goto (文1)
}
   :
   :

(式)が真なら、文1〜文99をとばして、文100を実行する。
ユーザーのその後の選択によって、文1〜文99のいずれかに分岐する。
goto文は使用すると『構造化プログラミングの規則を壊す』とか、『C言語の癌』とか言われ、かなり評判が悪いようですが、ディスクエラーやユーザーの判断による強制中止、デバッグのため強制的にプログラムの流れを変えたいなど、色々と使い道はあると思います。

例えば、ファイル読み書きの関数内で、ディスクエラーがあった場合、ディスクのエラー処理(フロッピーの挿入を促すメッセージと共に、<中止>か<リトライ>か<無視>かを選択するなど)を行い、その判断によってメインルーチンのさまざまな場所に復帰する場合などです。

このような、例外的処理は、プログラムのメインルーチンとは別の場所に一括してエラー処理ルーチンを作った方が、あとで見てわかりやすいのと、エラー後の行動をユーザーが自由に選べるというような、柔軟な設計にする事ができるようになるからです。

同様に、印刷ルーチンでユーザーがいつでも強制終了ができるようにしたり、入力ルーチンでユーザーが好きな場所から入力をやりなおせるという風に、ユーザーの使いやすい設計にするためには、gotoは不可欠だと思います。

ただし、慣れてくると、便利なものだからつい使わなくても良い所でもgotoを使ってしまいがちになりますが、例外処理でもない限り、できるだけgotoは使わずに済ませましょう。

ファイル

ファイルの入出力は高水準入出力関数と、低水準入出力関数があります。高水準関数は、いわゆるBASICでいうところのOPEN や PRINT# などに近いもので、互換性が重視されています。また、低水準関数はOSに依存しますが、高水準関数ではできないファイルの入出力が可能になります。

高水準関数
FILE構造体を使って、ファイルの読み書きを行います。FILE構造体とは、stdio.hで#defineで『struct _iobuf』と定義され、さらにその_iobufの構造が定義されています。ここには、ファイルハンドルやファイルの現在の読み書きポインター等が保存されています。

これを、自分のプログラムで使う場合に、

FILE *fp

というふうにして、_iobufの構造体へのポインタを宣言します。そして、ファイルをオープンする際に、fp=fopen(filename,"r")みたいにして、ポインターに代入します。以降のリード/ライトおよびクローズはすべてこのポインタ(ここではfp)を使って行います。
関数 書式 意味
fopen fopen(ファイル名,"モード")

モード:
"r"読み込み
"w"書き込み
"a"追加書き込み
ファイルをオープンし、確保したFILE構造体のポインタを返す。 if (NULL==(fp=fopen(filename,"r")))
{
printf("ファイルが見つかりません - %s \n",filename);
return;
}
fclose fclose(fp) fpが示すファイルをクローズする。 fclose(fp);
fgetc fgetc(fp) fpが示すファイルから1文字読み込む。 c=fgetc(fp);
fputc fputc(c,fp) fpが示すファイルに1文字書き出す。 fputc(c,fp);
fgets fgets(バッファ,最大文字数,fp) fpが示すファイルから1行読み込む。
(改行コードが来るか、ファイルの終わりになるか、最大文字数まで読み込む。)
if (NULL==fgets(buff,256,fp))
{
break;
}
fputs fputs(buff,fp) fpが示すファイルに、buffの内容を書き出す。 fputs(buff,fp);
fprintf fprintf(fp,"文字列%制御文字",変数) fpが示すファイルに、文字列および制御文字列で変換された変数の内容を書き出す。 fprintf(fp,"%-3u",zahyo);
fscanf fscanf(fp,制御文字",文字配列) fpが示すファイルから、制御文字で変換した文字列を、文字配列に読み込む。 fscanf(fp,"%d",a);
高水準入出力については、入力はキーボードからの入力である、getsやscanfなどがファイルからの読み込みになり、画面出力であるputsやprintfなどがファイルに出力されるものと考えればわかりやすいと思います。

この他、ランダムアクセスのための、ファイルポインターを移動させたり初期化したりする関数が用意されています。MS-DOSの、INT 21Hを使った事のある人には、なじみの深い項目が多いと思います。
関数 書式 意味
fseek fseek(fp,offset,モード)

モード:
0:ファイルポインタを、最初からoffsetの位置まで移動する。
1:ファイルポインタを現在位置からoffset分だけ後ろに移動する。
2:ファイルポインタを、最後からoffset分だけ後ろに移動する。
ファイルのポインタを、モードで指定した方法で、offset分だけ移動する。
ftell ftell(p) ファイルの現在のポインタを返す。返値はlongで返る。
rewind rewind(fp) ファイルポインタを先頭に戻し、エラーフラグとEOFフラグを初期化する。
低水準ファイル入出力
低水準ファイル入出力は、OSに依存するファイルの入出力を行います。たとえば、MS−DOS用のC言語では、INT 21Hでのファイルオープンに近い形でファイルの読み書きを行う事ができます。以下、MS-DOS用のMS−Cの例を示します。
関数 書式 意味
open open(ファイル名,アクセスコード,共有モード)

アクセスコード:
0000 読み込み
0001 書き込み
0010 読み書き
共有モード:
000 コンパチブル
001 読み書き禁止
010 書き込み禁止
011 読み込み禁止
100 読み書き許可
ファイルをオープンし、ファイルハンドルを返す。
アクセスコードや共有モードは、MS-DOSではALレジスタで指定するもの。

ファイル名は、あらかじめ配列もしくはポインター変数に代入しておく。
creat creat(ファイル名,属性)

属性:
0x01 読み込み専用
0x02 隠しファイル
0x04 システムファイル
0x08 ボリュームラベル
0x10 ディレクトリ
0x20 アーカイブ
openでは新規にファイルを作成する事ができないので、この関数でファイルを新規作成する。
既にファイルが存在している時は、既存のファイルは破棄される。
close close(ハンドル) ハンドルで示されたファイルハンドルをクローズする。
lseek lseek(ハンドル,移動量,方法)

方法:
0:ファイルポインタを、最初から移動量まで移動する。
1:ファイルポインタを現在位置から移動量だけ後ろに移動する。
2:ファイルポインタを、最後から移動量だけ後ろに移動する。
ハンドルで示されたファイルを、方法にしたがい、移動量だけ移動する。
read read(ハンドル,バッファ,バイト数) ハンドルで示されたファイルから、バッファに、バイト数分だけ読み込む。
write write(ハンドル,バッファ,バイト数) ハンドルで示されたファイルに、バッファから、バイト数分だけ書き込む。
低水準入出力では、MS−DOSとほぼ同等の入出力が行えるため、たとえばJFGAIJ.UFOの解読のような、アセンブラレベルで操作していたような処理まで、C言語で行えるようになります。

プログラム終了

C言語での「終わり」は、main()関数の終わりです。しかし、return命令でmain()関数を途中で終わらせることができます。さらに、途中で強制的にプロンプトに戻すために、exit()という関数が、標準関数に用意されています。exit(0)もしくはexit(1)を指定します。
exit(0); そのまま終了する。
exit(1); エラーのリカバリーをして終了する
ただし、コンパイラによっては一概に0が正常1が異常とは限りません。標準関数(stdlib.h)にはEXIT_SUCCESSとEXIT_FAILUREが定義されており、互換性を持たせるためには、正常終了の場合は
exit(EXIT_SUCCESS);
エラーのリカバリーが必要な時は
exit(EXIT_FAILURE);
と書く方が良いでしょう。

コンパイルとリンク

C言語は、ソースだけ見るとPerlやPHPに似てますが、大きく違う点はコンパイルとリンクが必要だという事です。つまり、C言語はコンパイル言語なので、インタプリタ言語とは違いソースをそのまま実行させる事はできません。コンパイルしてオブジェクトという中間ファイルを生成し、それをリンクして実行ファイルにします。

Linuxでは、
cc ソースファイル名
でコンパイルしますが、大きいプログラムを作る場合は1つの大きいソースをコンパイルするのではなく、複数のソースを分割してコンパイルします。これにより、何か不具合があった場合に、不具合のあった箇所のみコンパイルしなおす事ができ、また、不具合の範囲も限定され発見しやすくできます。さらに、分割で作成する事により、複数人による共同作業もしやすくなり、汎用的な関数(ライブラリ)を別のプログラムで流用しやすくなります。

分割コンパイルする場合、ソースから直接実行形式ファイルにはせず、いったんそれぞれのファイルのオブジェクトを生成しておき、最後にオブジェクト同士をリンクして1つの実行形式ファイルを作ります。この場合、
cc -c ソースファイル名 -o オブジェクトファイル名
という風に-cオプションを指定してコンパイルのみを行う指定をします。全てのオブジェクトができあがった後に、
cc オブジェクトファイル名 オブジェクトファイル名 オブジェクトファイル名… -o 実行ファイル名
というようにオブジェクトをリンクして実行ファイルにします。

しかし、複数のファイルをリンクする指示をその都度手動で行うのはいささか面倒です。そこで、コンパイルの指示を行うファイル(Makefile)を作ります。
CFLAGS = コンパイルオプション
all : 実行ファイル1 実行ファイル2 実行ファイル3…

実行ファイル1 : オブジェクト1 オブジェクト2 オブジェクト3…
   cc オブジェクト1 オブジェクト2 オブジェクト3… -o 実行ファイル1

     :
     :

オブジェクト1 : ソース1
   cc -c ソース1 -o オブジェクト1
     :
     :

clean :
   rm 実行ファイル群 オブジェクト群
最初にCFLAGS = でコンパイルオプションを指定する事で、以下のコンパイル全てに適応させる事ができます。

allというラベルを作り、そこで作りたい実行ファイルをスペース区切りで指定します。allというラベルがあると、単にmakeとすれば make all が指定されたのと同じ意味になります。その後に、実行ファイル1というラベル(※実際にはファイル名になります)に続いて、実行ファイル1を作るために必要なファイルを記述し、次の行から先頭をタブで字下げして実行ファイル1を作るための手順を書きます。

このように、何を作るためには何と何が必要という関係(依存性)を記述する事で、必要なファイルが足りない時に足りないものをコンパイルして生成したり、何が足りないのかというエラーを出してコンパイルをやめたり、ファイルのタイムスタンプが新しくなった時に、新しくなったファイルとそれを使っているファイルのみをコンパイルやリンクしなおすようになります。それにより、再コンパイルの時間を大幅に減らす事ができます。

ただし、依存性関係の記述を間違えてしまったり、タイムスタンプは古いけど実は別のバージョンのソースと入れ替えた、という場合には全てをコンパイルしなおしたいと思います。その場合に備えてcleanというラベルに続いて、その時に生成されるオブジェクトや実行ファイルを全て削除する手順を記述しておきます。これで、make cleanとすればオブジェクトと実行ファイルは削除されますので、再度makeとしてコンパイルを全部かけなおす事ができます。

終わりに

以上が、C言語の基本中の基本の部分です。まだまだこれだけでは、十分とはいえないのですが、C言語についてなんとなく理解できたでしょうか。

とにかく、C言語自体は基本の部分というのは非常に小さいもので、あとはプログラマーやその会社、プロジェクト単位で独自に関数を拡張して使うものです。これをお読みの方も、便利な関数を独自に開発してはいかがでしょうか。
スポンサーリンク