C言語入門|プリプロセッサ入門:インクルード処理と標準ライブラリ

― コンパイル前にひっそり働く縁の下の力持ち

これまでC言語のプログラムを書くとき、
ソースコードの先頭には、ほぼ決まり文句のように
#include <stdio.h>
と書いてきました。

正直なところ、
「とりあえず必要だから書いている」
という人も多いのではないでしょうか。

この章では、その #include をはじめとする
プリプロセッサの正体と、
標準ライブラリがどのように使われているのかを、
ビルドの流れに沿って整理していきます。

プリプロセッサとは何者か

プリプロセッサ(preprocessor)は、
コンパイラが動き出す前にソースコードへ前処理を行うソフトウェアです。

C言語のビルドは、大まかに次の順番で進みます。

順番処理内容
1プリプロセッサ
2コンパイラ
3リンカ

つまり、
コンパイラは、生のソースコードをそのまま読んでいるわけではない
という点が重要です。

ソースコードは、まずプリプロセッサによって加工され、
その結果がコンパイラへ渡されます。

プリプロセッサが行う主な仕事

プリプロセッサが行う処理はいくつかありますが、
特に重要なのは次の3つです。

処理内容説明
インクルード処理ヘッダファイルの内容を展開する。
マクロ処理#define による置換を行う。
条件付きコンパイル#if や #ifdef による分岐

これらの処理はすべて、
# で始まる命令を目印にして行われます。

プリプロセッサは、
高度な意味解析をするわけではなく、
基本的には「単純な置換」を黙々と繰り返しているだけです。

インクルード処理の正体

プリプロセッサの代表的な命令が、
#include です。

#include を書くと、
プリプロセッサは次のことを行います。

  • 指定されたファイルを探す。
  • そのファイルの内容を読み込む。
  • #include が書かれていた場所を、その内容で置き換える。

つまり、
#include は「ファイルを読み込む命令」ではなく、
ソースコードを丸ごと差し替える命令なのです。

実際にプリプロセッサの動作を見てみる

プリプロセッサの働きは、
GCCのオプションを使うと確認できます。

GCCの場合

次のようなシンプルなプログラムを用意します。

サンプルプログラム

// コメントA
#include <stdio.h>

// コメントB
int main(void)
{
    printf("Preprocessor test\n");
    return 0;
}

このファイルに対して、次のコマンドを実行します。

gcc -E -P -C sample.c
オプション意味
-Eプリプロセッサまでで処理を止める。
-P行番号の出力を抑制する。
-Cコメントを削除しない。

すると、画面には
stdio.h の内容が大量に展開されたソースコードが表示されます。

自分で書いたのは数行なのに、
コンパイラが実際に処理しているコードは、
何百行、何千行にも及んでいることがわかるでしょう。

Visual Studioの場合

Visual Studioの「開発者コマンドプロンプト」でも同様に確認することができます。
「開発者コマンドプロンプト」を使用すると PATH を通さなくてもコンパイルなどが実行できます。

「開発者コマンドプロンプト」は、タスクバー上の「スタート」から起動できます。

GCC(GNU Compiler Collection)の 「gcc -E -P -C」 オプションは、プリプロセッサのみを実行し、余計な情報を排除してコメントを残すという設定です。

Visual Studioのコンパイラである cl.exe(MSVC)でこれと同様の結果を得るには、以下のオプションを組み合わせて使用します。

対応するコマンド

開発者コマンドプロンプトで以下のコマンドを入力することで確認できます。

cl /E /EP /C sample.c

各オプションの解説

GCCとMSVC(Visual Studio)のオプションの対応関係は以下の通りです。

機能GCC オプションMSVC (cl.exe) オプション
プリプロセスのみ実行-E/E
行番号(#line)を出力しない-P/EP
コメントを保持する-C/C

コメントはコンパイラに届かない

ここで、もう1つ重要な事実があります。

プリプロセッサの段階で、
コメント文は削除されます。

つまり、

  • コメントは人間のためのもの
  • コンパイラはコメントを一切読んでいない。

ということになります。

どれだけ丁寧なコメントを書いても、
機械にとっては存在しない情報なのです。

標準ライブラリとは何か

画面に文字を表示する、
キーボード入力を受け取る、
ファイルを読み書きする。

こうした処理を、
毎回すべて自作するのは非現実的です。

そこでC言語には、
誰もが使うであろう基本機能をまとめた部品集が用意されています。
これが 標準ライブラリです。

ヘッダファイルの役割

標準ライブラリに含まれる関数は、
内部実装がそのまま見える形では提供されていません。

代わりに用意されているのが、
ヘッダファイルです。

ヘッダファイルには、次の情報が書かれています。

内容目的
関数名呼び出し時の名前を知らせる。
引数の型正しい使い方を保証する。
戻り値の型コンパイラによる型チェック

これは、以前学んだ
関数プロトタイプ宣言そのものです。

stdio.h は、
入出力に関する標準関数のプロトタイプ宣言を集めた
代表的なヘッダファイルです。

なぜインクルードが必要なのか

printf を使うとき、
#include <stdio.h> を書かなければならない理由は明確です。

  • プリプロセッサが stdio.h を展開する
  • printf のプロトタイプ宣言がソースに組み込まれる
  • コンパイラが正しい呼び出しだと判断できる

つまり、
インクルードは「使ってよい関数を事前に宣言するための仕組み」
なのです。

プリプロセッサは分業の要

プリプロセッサとヘッダファイルの仕組みがあるおかげで、

  • 複数のソースファイルに処理を分割できる。
  • 共通の宣言を1か所にまとめられる。
  • 修正の影響範囲を最小限にできる。

といった、
分業に強い開発スタイルが実現できます。