PR

 Cのプログラム先頭付近に,“#”で始まる命令がいくつか固まって書かれていることがある。中でもよく見かける#includeは,拡張子“.h”の「ヘッダー・ファイル」をソースコードに取り込む命令だ。「取り込まれるヘッダー・ファイルの中には宣言や定義が記されている」などと,本には書いてある。この宣言とか定義とは一体どのようなもので,どんな役割を果たしているのだろう? ヘッダー・ファイルと#で始まる命令について調査した。

Cのコンパイルは2段階に分かれている

 Cの,特にコマンドライン・インタフェースのプログラムには,

#include <stdio.h>

という1行が,半ば「おまじない」のように記述されている。もちろんこれはおまじないなどではないが,printfなどの関数やifなどの命令語とも違う。#includeや#defineのように,Cプログラムで使われる“#”の付いた予約語を「プリプロセサ指令(pre-processor directive)」と呼ぶ。

 Cプログラムのコンパイルは2段階に分かれていて,プリプロセサ指令はソースのほかの部分のコンパイルより先に処理される。具体的には,図1のようにまず「プリプロセサ(前処理プログラム)」がソースコードを読み込み,“#”で始まるプリプロセサ指令をすべて処理する。コンパイラが読み込んで機械語に変換するのは,このプリプロセサによる処理が済んだソースコードである。ただし,処理系によってはプリプロセサとコンパイラが一体化している場合もある。

図1●ソース内のプリプロセサ指令を処理してからコンパイルされる
図1●ソース内のプリプロセサ指令を処理してからコンパイルされる

 Cのプログラムを読み書きするときに最もよく目にするプリプロセサ指令は,おそらく“#include”だろう。#includeは,それが記述された個所に別のファイルを取り込めという指示である。これはCの入門書でイヤというほど解説されているので,知らない人はいないはずだ(と,思う)。

 例えば,図2(a)のような内容の“base.h”というファイルがあったとしよう。そして,(b)のようなCのソース“sample.c”が書かれたとする。“sample.c”の冒頭には“#include <base.h>”と記述してある。プリプロセサは#include指令の個所にbase.hを取り込む(c)。#include指令はその時点で削除される。取り込んだbase.hには#define指令が書かれているので,プリプロセサはそれに従ってソース中の文字列を置き換え,#define指令を削除する。すべてのプリプロセサ指令を処理し終えると,プリプロセサからコンパイラにソースが渡される。

図2●#include指令はその個所に指定された他のファイルの中身を取り込む
図2●#include指令はその個所に指定された他のファイルの中身を取り込む

 #includeで取り込んだファイルの中に,また#includeが書かれている場合もある。このため,プリプロセサが処理を終えてコンパイラに渡すソースコードには,元のソースコードからは想像も付かないほどたくさんのコードが追加されていることも珍しくない*1。たった10行程度のソースでも,コンパイルされるときには何百~何千行ものコードになっていたりするのだが,その部分はプログラマには見えない。

ヘッダーで関数をまとめて宣言する

 では,

#include <stdio.h>

というおまじないのような1行で取り込まれるstdio.hとは,どのような役割を持つファイルなのだろう?

 Cの処理系には,様々な関数があらかじめ「ライブラリ」として付属している。stdio.hの拡張子“.h”は「ヘッダー・ファイル」と呼ばれる種類のファイルに付くもので,ライブラリ内の関数を使うための宣言や定義が,ライブラリの機能や用途ごとにまとめて記述されている。stdio.hは,ライブラリのうち,キーボードからの入力やディスプレイへの出力,ファイルの操作などいわゆる標準入出力関係の関数群を記録した「標準入出力ライブラリ」に対応するヘッダー・ファイルだ。ファイル名の“stdio”は,「standard input/output(標準入出力)」の略である。

 このようなヘッダー・ファイルが必要になるのは,Cで関数を呼び出す際には,あらかじめその関数を宣言しておかなければならないためだ。例えば,ある関数が同じファイル内の関数を呼び出そうとしているときには,あらかじめ

関数の型 関数名(引数の型リスト);

という形で関数を宣言しておかなければならない。

 ただし,関数を呼び出すコードより前に関数の定義が記述されていれば,このような宣言は必要ない。リスト1(a)ではmain関数の後にmain関数から呼び出される関数vprintを定義しているが,(b)のように,main関数の前に関数の定義を記述すると,その段階で宣言されたものと見なされる。

 一方,ほかのソース・ファイルで定義された関数(「外部関数」と呼ぶ)を使用するには,以下のようにexternというキーワード*2を付けた宣言が必要になる。

(a)mainの後でvprintが定義されている(宣言が必要)
#include<stdio.h>

/* 関数の宣言 */
void vprint(char *);

int main(void)
{
  char str[] = "Hello!";
  vprint(str);    /* 関数の呼び出し */
}

/* 関数の定義 */
void vprint(char *str)
{
  char *p=str;
  while (*p != 0) {
    putc((int)*p, stdout);
    putc((int)'\n', stdout);
    p++;
  }
}

(b)mainより前にvprintが定義されている(宣言は不要)
#include <stdio.h>

/* 関数の定義(宣言も兼ねる) */
void vprint(char *str)
{
  char *p=str;
  while (*p != 0) {
    putc((int)*p, stdout);
    putc((int)'\n', stdout);
    p++;
  }
}

int main(void)
{
  char str[] = "Hello!";
  vprint(str);    /* 関数の呼び出し */
}
リスト1●関数の宣言。main関数から呼び出すvprint関数の定義がmain 関数より後ろにあるので宣言が必要になる(a)。main関数より先にvprint 関数を定義してある場合は改めて宣言する必要はない(b)

extern 関数の型 関数名(引数の型リスト);

 ライブラリ内の関数はすべて外部関数となるため,それらを使用するにはこのようなextern宣言が必要になる*3。しかし,ライブラリ関数の仕様をいちいち調べて宣言を書くのは手間がかかる。そこでライブラリ関数のextern宣言をまとめて記述したヘッダー・ファイルが用意されているわけだ。プログラマは自分が利用したいライブラリに対応するヘッダー・ファイルを読み込む#include指令を書いておけば,それで必要な宣言が済んだことになる。