Top
C
Cのコンパイルを知る
プリプロセッサを知る
ポイント
プリプロセッサを知る
C言語のコンパイル前に行われる前処理工程のプリプロセッサについて学ぶための記事
-
Point 1プリプロセッサはコンパイルの前処理プリプロセッサはC言語でコンパイルをするまえに行われる前処理で、値の置き換えやファイルの読み込み、条件によるコードの書き換えなどを実行
-
Point 2#defineや#includeの処理#defineのようなマクロや#include, #ifなどもこの段階で処理
ステップ概要
プリプロセッサを知る
Cのコンパイルの流れについて簡単に解説します.
プリプロセッサを経てどのようにソースコードが変わるかを解説します
#define(マクロ)の役割について解説します.
#includeのファイルを貼り付ける処理について解説します.
#if #elseの条件によってコードを切り替える処理について解説します.
Step
Cのコンパイル全体の流れを知る
まず初めにCのコンパイル全体の流れについて解説します.
以下のようにCのコンパイルには、「プリプロセッサ」「コンパイル」「アセンブリ」「リンカ」の4工程があります.これらの処理をまとめて一般的に「コンパイル」や「ビルド」と言います.
簡単に各処理についてまとめます.これらの処理はgccなどのコマンドによって全て通して実行することができますが、オプションをつけることで途中生成物を得ることも可能です.
まず人間が.cや.hのソースコードを書きます.その後そのファイルをプリプロセッサで処理して.iファイルを得ます.
.iファイルによってコンパイラが読めるコードになったので、これらを用いて次にコンパイラで人間でも読みにくいが読み書きできるアセンブリ言語で書かれたアセンブリファイル(.sファイル)を生成します.
次に.sファイルをアセンブラによって人間には読めないバイナリデータである.oファイル/オブジェクトファイルに変換されます.
最後にこれらの複数の.oファイルを集めてリンクするリンカー処理で実行ファイルを生成します.
以下のようにCのコンパイルには、「プリプロセッサ」「コンパイル」「アセンブリ」「リンカ」の4工程があります.これらの処理をまとめて一般的に「コンパイル」や「ビルド」と言います.
画像 : Cのコンパイル概要
各処理の概要
簡単に各処理についてまとめます.これらの処理はgccなどのコマンドによって全て通して実行することができますが、オプションをつけることで途中生成物を得ることも可能です.
まず人間が.cや.hのソースコードを書きます.その後そのファイルをプリプロセッサで処理して.iファイルを得ます.
ポイント : プリプロセッサとは
- .c/.hファイルを.iファイルに編集する前処理
- .iファイルは.cに似て読みやすいファイル
- コンパイル前に実行され主にマクロやincludeの処理を行う
- gcc -E でプリプロセッサ後の出力を得られる
ポイント : コンパイルとは
- .iファイルを.sファイルに変換する処理
- .sファイルは機械が理解できる命令を人間が読める形で書いたファイル
- Cで書かれた高水準なプログラムをパソコンに読める低水準なプログラムに変換
- gcc -S でコンパイル後の出力を得られる
ポイント : アセンブラとは
- .sファイルを.oファイルに変換する処理
- .oファイルは機械が実行できる形で書いたファイル
- アセンブラ言語で書かれた低水準なプログラムを機械語に変換
- gcc -c でアセンブラ後の出力を得られる
ポイント : リンカとは
- .oファイルから最終的な実行ファイルを生成する処理
- 機械語で書かれたプログラムを集めて実行ファイルを生成
Step
プリプロセッサの役割を知る
それではプリプロセッサの役割についてです.
プリプロセッサはC言語の文法などとは全く関係なく処理を行います.コンパイルのようにC言語の文法に問題があっても一切のエラーを出力しません.コンパイルはC言語の文法の確認して翻訳するような作業をするのに対して、プリプロセッサはソースコードを言われたままに改変するような作業を行います.
プリプロセッサは文字列を置き換えたり、コメント行を消したり、設定によって一部のソースコードを削除したり残したりと様々な処理をします.
下記にプリプロセッサの処理をまとめます.
これらの処理を経てコンパイラが読めるコードのみに変換しています.人間が読みやすくする工夫としてコードに書いていた「コメント」「PIなどの定数表現」「繰り返される処理のまとめ」を正しい形に整形するために行われています.
コメント行の削除や余計な空白の削除は明白なので、それ以外の処理について残るステップで解説していきます
因みにC言語のプリプロセッサはCPreProcessorと呼ばれC++言語と混同しやすいですが"CPP"と呼ぶこともあります.
プリプロセッサはC言語の文法などとは全く関係なく処理を行います.コンパイルのようにC言語の文法に問題があっても一切のエラーを出力しません.コンパイルはC言語の文法の確認して翻訳するような作業をするのに対して、プリプロセッサはソースコードを言われたままに改変するような作業を行います.
プリプロセッサは文字列を置き換えたり、コメント行を消したり、設定によって一部のソースコードを削除したり残したりと様々な処理をします.
下記にプリプロセッサの処理をまとめます.
ポイント : プリプロセッサの処理とは
- コメント行の削除
- 余計な空白の削除
- #defineによるマクロ、置換処理
- #includeによるファイル内容の挿入
- #if#elseによるコードの削除などの改変
- 等々
画像 : プリプロセッサ例
因みにC言語のプリプロセッサはCPreProcessorと呼ばれC++言語と混同しやすいですが"CPP"と呼ぶこともあります.
Step
#defineの役割を知る
#defineは主に文字列の置き換えなどを行います
コード上で3.141592と言った数字をそのまま書かれるのは嫌われていたり、毎回書くのを防ぐため、大抵の場合はPIのように意味のわかりやすい表記で記載します.そのときに#defineを使って定数PIを定義します.
#defineで定義されたものは何でもかんでも置換するわけではありません.適切に""で括られている文字列は無視し、また完全一致したマクロ定義しか置換しないので部分的に一致している文字があっても問題はありません.CPPの中ではそれらのためにちゃんとした構文解析を行った上でプリプロセスを実行しています.
他には引数を伴うマクロとして処理を展開したりすることにも用いられます.
上記のようにADDで定義されたマクロが引数を10と20として展開されていて、適切なコードになっています.複数行連ねることも可能でその場合は"\(バックスラッシュ)"で文末を改行します.
あとは文字と文字の結合も##を使うことによって可能で以下のようなこともできます.
以上が#defineを使ったときの様々な使われ方でした.
#defineで定義するときは必ず()をつけるようにします.上のAREAのようなことをしていないと以下の例のような場合に思っていない形に展開されてしまうからです.
このように想定した処理にならないと言った不具合につながるためです.
関連として、これらの定義を削除するのは#undefです.新しく値を付け直したりすることができます.
コード上で3.141592と言った数字をそのまま書かれるのは嫌われていたり、毎回書くのを防ぐため、大抵の場合はPIのように意味のわかりやすい表記で記載します.そのときに#defineを使って定数PIを定義します.
C : C言語における#defineの使われ方
#define PI 3.141592
int arc = 2 * PI * radius;
//int arc = 2 * 3.141592 * radius;と置き換えられます
他には引数を伴うマクロとして処理を展開したりすることにも用いられます.
C : C言語における#defineの使われ方2
#define ADD(x, y) ( x + y )
int result = ADD(10, 20);
//int result = ( 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で定義するときは必ず()をつけるようにします.上のAREAのようなことをしていないと以下の例のような場合に思っていない形に展開されてしまうからです.
C : #defineの値を()で括らないと...
#define ADD(x, y) ( x + y )
int result = ADD( 10, 20 ) / 10;
//int result = 10 + 20 / 10;となってしまいます.
#defineで定義したマクロ定義の削除
関連として、これらの定義を削除するのは#undefです.新しく値を付け直したりすることができます.
Step
#includeの役割を知る
次に#includeについてです.
#includeは指定したファイルの内容をそのまま貼り付ける動きをします.
下記で一目瞭然かと思います.
そのためcsvを貼り付けて配列をそのまま初期化するようなことも行われることがあります.
#includeは括弧(ブラケット)<>で括る書き方とダブルクオテーション""で括る書き方の二つの書き方があります. この違いは、前者<>はあらかじめ環境の設定で決められているフォルダからファイルを探してくるのに対して、後者""は自分のファイルが置かれている周りからファイルを探そうとしてくる点です.前者はコンパイラなどが決めている標準ヘッダから探し、後者は自作したヘッダファイルから優先して探すことになります.
自分が作ったファイルは""、そうでないものは<>で問題ありません.
#includeは愚直にファイルの中身を貼り付けていくため、何もしないとそのまま同じファイルの中身を何度も貼り付けてしまい、二重定義などになってしまいます.
そのためにヘッダーファイルで以下のような記述をすることが多いです.
上記の意味は極めて簡単です. #ifndefの行は、「もし__SAMPLE_H__が定義されていなければ」、#defineの行は、「__SMAPLE_H__を定義する」となっているだけです.もし__SAMPLE_H__が一度でも定義されていれば#ifndefから#endifの中身は削除され空白のページになります.
__SAMPLE_H__のようなマクロ定義名は他と被らないようにファイル名を使って適当に作ります.特にルールはありませんが二つのアンダーバーで囲むことが多いです. ただし最近は#pragma onceと書く方法でそれを代替することも可能なことがあり、浸透してきました.
#includeは指定したファイルの内容をそのまま貼り付ける動きをします.
下記で一目瞭然かと思います.
画像 : #include処理
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
__SAMPLE_H__のようなマクロ定義名は他と被らないようにファイル名を使って適当に作ります.特にルールはありませんが二つのアンダーバーで囲むことが多いです. ただし最近は#pragma onceと書く方法でそれを代替することも可能なことがあり、浸透してきました.
ポイント : #includeのポイント
- ファイルの中身を貼り付ける処理
- ""で自分で作ったファイル, <>でそれ以外のファイルを指定
- .h以外も読み込める
- #includeで何度も読み込まれないように#ifndefと#defineを使ったり, #pragma onceを使用する.
Step
#if #elseの役割を知る
最後に#if#else等の処理についてまとめます.
#if #else #elif #ifdef #ifndefといったこれらの処理は コードとして使う部分使わない部分を選択して使わない部分を削除するときなどに使用します.
具体的にはWindows用のコード、Mac用のコード、組み込み用のコードのほとんどが共通して使えるのに ある部分のみ環境ごとの処理を切り替えなくてはならないときなどが挙げられます.
上記のようなコードではWINDOWSというマクロが定義されていれば上のprintfだけが残り、定義されていなければ下のprintfだけが残ります.
各機能(ディレクティブ)の使い方は下記にまとめます.
#if definedが#ifdefに等しく、#if not defined が#ifndefに等しいです.
#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
各機能(ディレクティブ)の使い方は下記にまとめます.
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
//終了
Done