あなたに次の選択肢を用意するサイト
Thoth Coworker
~ プログラミングの次++ ~
For
新社会人 / 新学生 / 新院生 / 新研究者
カテゴリ
プリプロセッサを知る
Facebookシェア Twitterツイート LINEで送る
P
ポイント
プリプロセッサを知る
C言語のコンパイル前に行われる前処理工程のプリプロセッサについて学ぶための記事
  • Point 1
    プリプロセッサはコンパイルの前処理
    プリプロセッサはC言語でコンパイルをするまえに行われる前処理で、値の置き換えやファイルの読み込み、条件によるコードの書き換えなどを実行
  • Point 2
    #defineや#includeの処理
    #defineのようなマクロや#include, #ifなどもこの段階で処理
P
ステップ概要
プリプロセッサを知る
Cのコンパイルの流れについて簡単に解説します.
プリプロセッサを経てどのようにソースコードが変わるかを解説します
#define(マクロ)の役割について解説します.
#includeのファイルを貼り付ける処理について解説します.
#if #elseの条件によってコードを切り替える処理について解説します.
Step
1
Cのコンパイル全体の流れを知る
まず初めにCのコンパイル全体の流れについて解説します.

以下のようにCのコンパイルには、「プリプロセッサ」「コンパイル」「アセンブリ」「リンカ」の4工程があります.これらの処理をまとめて一般的に「コンパイル」や「ビルド」と言います.
画像 : Cのコンパイル概要
各処理の概要

簡単に各処理についてまとめます.これらの処理はgccなどのコマンドによって全て通して実行することができますが、オプションをつけることで途中生成物を得ることも可能です.

まず人間が.cや.hのソースコードを書きます.その後そのファイルをプリプロセッサで処理して.iファイルを得ます.
ポイント : プリプロセッサとは
  • .c/.hファイルを.iファイルに編集する前処理
  • .iファイルは.cに似て読みやすいファイル
  • コンパイル前に実行され主にマクロやincludeの処理を行う
  • gcc -E でプリプロセッサ後の出力を得られる
.iファイルによってコンパイラが読めるコードになったので、これらを用いて次にコンパイラで人間でも読みにくいが読み書きできるアセンブリ言語で書かれたアセンブリファイル(.sファイル)を生成します.
ポイント : コンパイルとは
  • .iファイルを.sファイルに変換する処理
  • .sファイルは機械が理解できる命令を人間が読める形で書いたファイル
  • Cで書かれた高水準なプログラムをパソコンに読める低水準なプログラムに変換
  • gcc -S でコンパイル後の出力を得られる
次に.sファイルをアセンブラによって人間には読めないバイナリデータである.oファイル/オブジェクトファイルに変換されます.
ポイント : アセンブラとは
  • .sファイルを.oファイルに変換する処理
  • .oファイルは機械が実行できる形で書いたファイル
  • アセンブラ言語で書かれた低水準なプログラムを機械語に変換
  • gcc -c でアセンブラ後の出力を得られる
最後にこれらの複数の.oファイルを集めてリンクするリンカー処理で実行ファイルを生成します.
ポイント : リンカとは
  • .oファイルから最終的な実行ファイルを生成する処理
  • 機械語で書かれたプログラムを集めて実行ファイルを生成
Step
2
プリプロセッサの役割を知る
それではプリプロセッサの役割についてです.

プリプロセッサはC言語の文法などとは全く関係なく処理を行います.コンパイルのようにC言語の文法に問題があっても一切のエラーを出力しません.コンパイルはC言語の文法の確認して翻訳するような作業をするのに対して、プリプロセッサはソースコードを言われたままに改変するような作業を行います.

プリプロセッサは文字列を置き換えたり、コメント行を消したり、設定によって一部のソースコードを削除したり残したりと様々な処理をします.
下記にプリプロセッサの処理をまとめます.
ポイント : プリプロセッサの処理とは
  • コメント行の削除
  • 余計な空白の削除
  • #defineによるマクロ、置換処理
  • #includeによるファイル内容の挿入
  • #if#elseによるコードの削除などの改変
  • 等々
これらの処理を経てコンパイラが読めるコードのみに変換しています.人間が読みやすくする工夫としてコードに書いていた「コメント」「PIなどの定数表現」「繰り返される処理のまとめ」を正しい形に整形するために行われています.

画像 : プリプロセッサ例
コメント行の削除や余計な空白の削除は明白なので、それ以外の処理について残るステップで解説していきます

因みにC言語のプリプロセッサはCPreProcessorと呼ばれC++言語と混同しやすいですが"CPP"と呼ぶこともあります.
Step
3
#defineの役割を知る
#defineは主に文字列の置き換えなどを行います

コード上で3.141592と言った数字をそのまま書かれるのは嫌われていたり、毎回書くのを防ぐため、大抵の場合はPIのように意味のわかりやすい表記で記載します.そのときに#defineを使って定数PIを定義します.
C : C言語における#defineの使われ方
    
    #define PI 3.141592
    int arc = 2 * PI * radius;
    //int arc = 2 * 3.141592 * radius;と置き換えられます
    
  
#defineで定義されたものは何でもかんでも置換するわけではありません.適切に""で括られている文字列は無視し、また完全一致したマクロ定義しか置換しないので部分的に一致している文字があっても問題はありません.CPPの中ではそれらのためにちゃんとした構文解析を行った上でプリプロセスを実行しています.

他には引数を伴うマクロとして処理を展開したりすることにも用いられます.
C : C言語における#defineの使われ方2
    
    #define ADD(x, y) ( x + y )
    int result = ADD(10, 20);
    //int result = ( 10 + 20 );と置き換えられます
    
  
上記のようにADDで定義されたマクロが引数を10と20として展開されていて、適切なコードになっています.複数行連ねることも可能でその場合は"\(バックスラッシュ)"で文末を改行します.

あとは文字と文字の結合も##を使うことによって可能で以下のようなこともできます.
C : C言語における#defineの使われ方3
    
    #define ABCD 100
    #define ADD(x, y) ( x##y )
    int result = ADD(AB, CD);
    //int result = ABCD;と置き換えられ、int reuslt = 100となります.
    
  
以上が#defineを使ったときの様々な使われ方でした.
ポイント : #defineのポイント
  • 文字列の置換やマクロの展開を行う
  • 複数行になるときはを各行の末尾に
  • 定義は()で括る
#defineで定義するときの注意

#defineで定義するときは必ず()をつけるようにします.上のAREAのようなことをしていないと以下の例のような場合に思っていない形に展開されてしまうからです.
C : #defineの値を()で括らないと...
    
    #define ADD(x, y) ( x + y )
    int result = ADD( 10, 20 ) / 10;
    //int result = 10 + 20 / 10;となってしまいます.
    
  
このように想定した処理にならないと言った不具合につながるためです.
#defineで定義したマクロ定義の削除

関連として、これらの定義を削除するのは#undefです.新しく値を付け直したりすることができます.
Step
4
#includeの役割を知る
次に#includeについてです.

#includeは指定したファイルの内容をそのまま貼り付ける動きをします.
下記で一目瞭然かと思います.

画像 : #include処理
そのためcsvを貼り付けて配列をそのまま初期化するようなことも行われることがあります.
C : #includeでcsvファイルの中身をコードに貼り付ける
    
    int numarray[] = {
    #include "data.csv"
    };
    
    //これは下記のようになる
    
    int numarray[] = {
    1, 2, 3, 4, 5, 6, 7, 8, 9
    };
    
  
#include <>と#include ""の違い

#includeは括弧(ブラケット)<>で括る書き方ダブルクオテーション""で括る書き方の二つの書き方があります. この違いは、前者<>はあらかじめ環境の設定で決められているフォルダからファイルを探してくるのに対して、後者""は自分のファイルが置かれている周りからファイルを探そうとしてくる点です.前者はコンパイラなどが決めている標準ヘッダから探し、後者は自作したヘッダファイルから優先して探すことになります.
自分が作ったファイルは""、そうでないものは<>で問題ありません.
#includeで何度も読み込まれないようにする

#includeは愚直にファイルの中身を貼り付けていくため、何もしないとそのまま同じファイルの中身を何度も貼り付けてしまい、二重定義などになってしまいます.
そのためにヘッダーファイルで以下のような記述をすることが多いです.
C : 何度もヘッダファイルが読み込まれるのを防ぐ
    
      #ifndef __SAMPLE_H__
      #define __SAMPLE_H__
      int x;
      int func();
      #endif
    
  
上記の意味は極めて簡単です. #ifndefの行は、「もし__SAMPLE_H__が定義されていなければ」、#defineの行は、「__SMAPLE_H__を定義する」となっているだけです.もし__SAMPLE_H__が一度でも定義されていれば#ifndefから#endifの中身は削除され空白のページになります.
__SAMPLE_H__のようなマクロ定義名は他と被らないようにファイル名を使って適当に作ります.特にルールはありませんが二つのアンダーバーで囲むことが多いです. ただし最近は#pragma onceと書く方法でそれを代替することも可能なことがあり、浸透してきました.
ポイント : #includeのポイント
  • ファイルの中身を貼り付ける処理
  • ""で自分で作ったファイル, <>でそれ以外のファイルを指定
  • .h以外も読み込める
  • #includeで何度も読み込まれないように#ifndefと#defineを使ったり, #pragma onceを使用する.
Step
5
#if #elseの役割を知る
最後に#if#else等の処理についてまとめます.

#if #else #elif #ifdef #ifndefといったこれらの処理は コードとして使う部分使わない部分を選択して使わない部分を削除するときなどに使用します.
具体的にはWindows用のコード、Mac用のコード、組み込み用のコードのほとんどが共通して使えるのに ある部分のみ環境ごとの処理を切り替えなくてはならないときなどが挙げられます.
C : #if #elseの使いどき
    
      int x = 0;
      #ifdef WINDOWS
      printf("This is Windows %d ", x);
      #else
      printf("This isnot Windows %d ", x);
      #endif
    
  
上記のようなコードではWINDOWSというマクロが定義されていれば上のprintfだけが残り、定義されていなければ下のprintfだけが残ります.
各機能(ディレクティブ)の使い方は下記にまとめます.
C : 各ディレクティブの使い方
    
      #if DEBUG_LEVEL > 5
      //もしDEBUG_LEVELが5より大きいなら
      #elif DEBUG_LEVEL == 4
      //もしDEBUG_LEVELが4に等しいなら
      #else
      //それ以外なら
      #endif
      //終了
      
      #ifdef WINDOWS
      //#if defined(WINDOWS)と等価で, WINDOWSというマクロが定義されているなら
      #elif MAC
      //#if defined(MAC)と等価で, MACというマクロが定義されているなら
      #else
      //それ以外なら
      #endif
      //終了

      #ifndef LINUX
      //#if not defined(LINUX)と等価で, LINUXというマクロが定義されているなら
      #endif
      //終了
    
  
#if definedが#ifdefに等しく、#if not defined が#ifndefに等しいです.
Done