C LANGUAGE TECHNOLOGY

【C言語】プリプロセッサ(マクロ)とは

悩んでいる人

C言語のプリプロセッサ(マクロ)を教えて!

こういった悩みにお答えします.

本記事の信頼性

  • リアルタイムシステムの研究歴12年.
  • 東大教員の時に,英語でOSの授業.
  • 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校コンピュータサイエンス学部2021年の世界大学学術ランキングで20位)で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発
  • プログラミング歴15年以上,習得している言語: C/C++Solidity,Java,Python,Ruby,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
  • 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」GitHubにオープンソースとして公開

こういった私から学べます.

C言語のプリプロセッサ(マクロ)とは

プリプロセッサ(マクロ)とは,C言語でコンパイルする前の処理(プリプロセス)を行うプログラムのことです.

プリプロセッサは,マクロ置換(記号定数,引数付きマクロ)や,ファイルの取り込み(インクルード)等を行います.

また,プリプロセッサには,if文に相当するような簡単な制御構造があり,条件付きコンパイル等が行えます.

プリプロセッサは,処理を行単位で行います.

プリプロセッサへの指示を行うための予約語は,最初の文字が’#’で必ず始まります.

ここで,#の前に空白はあってもよいです.

’#’が最初に付いているトークン(token:文字綴り)から始まる行は,プリプロセッサにより何らかの処理をされて,コンパイラに渡されます.

これらの行は,C言語の残りの部分とは文法的に独立しています.

また,プリプロセッサの適用範囲は,ファイルの全て(最初から最後まで)です.

C言語のプリプロセッサには以下の予約語があります.

  • #define
  • #undef
  • #include
  • #if
  • #elif
  • #else
  • #ifdef
  • #ifndef
  • #endif
  • #line
  • #error
  • #pragma
  • #

※#lineは行番号を変更する機能なので,基本的には使わない方が無難です.

それでは,これらを組み合わせたマクロの使い方を紹介していきます.

#include:ファイルの取り込み

ファイルの取り込み(インクルード)は

または

で行えます.

ファイルをインクルードすると,その位置にファイルが埋め込まれるので,読み込まれたファイルがその場所に記述されていた場合と同じ処理をします.

filenameを<>で囲むと,処理系の規則に従いヘッダファイルがあるパスが検索されます.例えば,UNIXの処理系の場合,/usr/include等のデフォルトのディレクトリや環境変数「C_INCLUDE_PATH」で指定したディレクトリ等が検索されます.

filenameを””で囲むと,まず現在のディレクトリが検索され,次に上記の処理系の規則に従ったパスが検索されます.

また,インクルードされるファイルにさらに#includeがあっても構いません.

つまり,#includeはネスト(入れ子)が可能です.

また,ファイルへの取り込みはヘッダファイル(*.h)だけでなくソースファイル(*.c)も可能です.

インクルードするファイルは,関数プロトタイプ宣言やグローバル変数のextern宣言をまとめたヘッダファイルにするのが一般的です.

ヘッダファイルをインクルードする全てのソースファイルで共通して利用できるため,ファイル間の不整合によるバグを除去できます.

#define:マクロ置換

マクロ置換は#defineで行います.

#defineはトークンの置換を行っているにすぎないことを,きちんと理解しましょう.

記号定数マクロ

記号定数マクロのコードは以下になります.

MACROトークンを5と定義しています.

実行結果は以下になります.

MACROトークンの値が5と出力されていることがわかります.

マクロの入れ子(多重定義)

マクロは,入れ子ができます(多重定義).

例えば,以下のトークンを定義した場合を説明します.

まず,コード中のAは,Bに置換されます.

次に,BはCに置換されるので,最終的にAはCに置換されます.

多重定義では,どちらが先に定義されているかは関係ありません.

下記のように逆順で定義する場合でも,AはCに置換されます.

プリプロセッサは置換の際の不整合が生じないように何回もスキャンします.

マクロの入れ子(多重定義)した定数を出力するコードは以下になります.

実行結果は以下になります.

全ての出力結果が1になっていることがわかります.

マクロ関数(引数付きマクロ)

#defineは単なる文字列の置換だけでなくマクロ関数(引数付きのマクロ)を定義することで,柔軟なコードを書くことができます.

例えば,2乗を求めるsquareマクロ関数を定義します.

以下のように書くと,引数xの2乗を返すsquareマクロ関数を定義できます.

square関数マクロの利用例は以下になります.

上記のコードは,下記のように展開されて,2乗を計算します.

このように,マクロの良い点は関数のように引数の型に依存しないことです.

つまり,squareは,int型でもdouble型でも利用できます.

※マクロはC++言語のテンプレートと似ています.

int型で同じ動作をする関数は以下のコードです.

関数呼び出しの場合,レジスタの退避や復帰処理のようなオーバーヘッドが発生してしまいます.

これに対して,マクロ関数の場合,単に展開され置換されるだけなのでオーバーヘッドがなく,関数より高速に処理できます.

その分,マクロ関数を利用すると,メモリサイズが大きくなる傾向にあります.

プリプロセッサ(マクロ)の副作用

マクロはテキストを単に置換しますが,それに伴う副作用があります.

例えばsqr関数マクロを以下のように定義した場合を考えてみましょう.

例えば,以下のように計算するとします.

※正しい計算結果は5^2 = 25になります.

この場合,プリプロセッサは以下のように展開します.

計算結果は11となり,正しい計算結果の25になりません.

上記のマクロ関数squareのように定義するとプリプロセッサは以下のように展開し,正しい計算結果25となります.

このようにマクロを利用するとカッコ()の使い方が重要になります.

あなたがマクロを利用してコードを書く時は,十分に気をつけて下さい.

カッコの使い方が正しかったとしても,以下の場合はどうなるでしょうか?

この場合,プリプロセッサは以下のように展開します.

これでは,yの値もxの値も間違って計算してしまいます.

マクロを上手に書き換えれば避けられるわけではなく,マクロの本質的な問題(副作用)になります.

したがって,マクロを利用する時は正しく理解することが重要になります.

squareとsqr関数マクロのコードを実行して動作を確認してみましょう.

実行結果は以下になります.

コンパイル時に21行目でマクロ関数square内でインクリメントを利用しているので,未定義の動作になる警告が出ています(2~10行目).

14行目のマクロ関数squareの実行結果は16(=4^2)になり,実行後のxの値は4になっています.(9(=3^2)になりません.)

変数xが前置インクリメントを2回実行してしまったことが原因です.

このように,マクロの副作用が発生することを正しく理解しましょう.

マクロの副作用によるバグを避けるために,現在のC言語の実装ではインライン関数が好まれます.

squareをインライン関数で定義すると以下になります.

square関数の定義の前に「static inline」が追加されていることを確認して下さい.

インライン関数で実装することで,マクロと同様にコンパイラはsquare関数を展開した高速なプログラムを生成します.

しかし,インライン関数も通常の関数と同様に型を指定する必要があること,コンパイラの最適化オプションやメモリサイズの制約等により展開されずに通常の関数と同じ動作になることがありますので注意して下さい.

#undef,#define:トークンの再定義

トークンの再定義をしたい場合,#undef,#defineを組み合わせて利用します.

以前に定義されていたトークンがある場合,同じトークンを#defineで定義をするとコンパイルエラーになってしまうので,#undefで一度トークンを削除して,#defineで同じトークンを定義します.

トークンの再定義は,明示的に定義したトークンを利用したい場合(定義を上書きしたい場合)に使います.

OSトークンの再定義は以下のコードです.

7行目に#defineでOSを"Windows"と定義,8行目に#undefでOSの定義を削除,8行目に#defineでOSを"Linux"と定義しています.

実行結果は以下になります.

再定義された"Linux"が出力されていることがわかります.

ここで,コードの7行目の#defineをコメントアウトしても正常に動作することに注意して下さい.

#undefはマクロが定義されていた場合は削除しますが,未定義の場合は何も削除しません.

8行目の#undefをコメントアウトしたら,以下のようにコンパイル時に警告が発生します(2~7行目).

実行すると再定義された"Linux"が出力されます(9行目).

行末に「\」:複数行のマクロと副作用を回避するdo while文

ファイルのオープンやクローズ等もお決まりのパターンなので,マクロにしてみましょう.

例えば,以下のようにfile_openマクロ関数を定義したとします.

このマクロ関数file_openの引数は以下になります.

  • 第1引数fp:ファイルポインタ
  • 第2引数fname:ファイル名
  • 第3引数mode:モード
  • 第4引数errno:エラーコード

ファイル名がfnameのファイルをモードmodeでオープンし,ファイルポインタをfpに代入します.

ファイルオープンに失敗した時にはメッセージを出力した後にexit関数を利用したエラーコードerrnoで強制終了します.

これらの一連の処理は,ファイルをオープンする時には必ず行うのでマクロ関数にするメリットはあります.

しかし,上記のマクロは1行で書かれていてとても読みにくいですよね.

プログラムの開発効率や保守性という観点から考えると,読みやすさも重要です.

そこで,#defineで定義が長くなる場合は行末に「\」を置けば次の行に続けることができます.

この機能により,上記のマクロ関数は以下のように定義できます.

このように記述すれば,行の終わりに「\」が置かれる以外は,普通のC言語のプログラムと同じです.

こちらの方が,とても読みやすいですよね.

しかし,このマクロは失敗することがあります.

関数を呼び出す際には,最後にセミコロン;を書くためです.

するとマクロを展開した時に,最後のセミコロンが冗長になってしまいますが,場所によっては邪魔になり,副作用(予期せぬ結果)を引き起こすことがあります.

以下に副作用が発生するコードを紹介します.

引数を1個(argcは2)入力し,そのファイルを読み込む処理をしています.

引数が1個以外の場合はprint_usage関数を呼び出してexit関数で強制終了します.

上記のコードは,一見すると正常にコンパイルできそうですが,以下のようにコンパイルエラーになります.

この理由として, このプログラムのmain関数は以下のように展開されるからです.

file_open関数マクロで展開された部分(上記の7~10行目)をよく見てみましょう.

10行目の最後にセミコロン;が入っていることがわかります.

これが副作用になって,正常にコンパイルできませんでした.

このセミコロンを除去した場合でも,11行目のelse文は6行目のif文に対する処理ではなく,7行目のfile_openマクロのif文に対する処理になってしまいます.

なので,コンパイルは成功しても正常に実行できません.

そこで,以下のようにdo while文を利用すると,この副作用を防ぐことができます .

※防御的プログラミングにより,if文に複文のカッコ「{}」を利用して複文にする方法でも可能です.

実行結果は以下になります.

※何もしていないので何も出力されません.

#if,#elif,#else,#error,#endif:条件付きコンパイル

#if,#elif,#else,#error,#endifを利用した条件付きコンパイルを紹介していきます.

OSTYPEの値によりOSトークンを定義するコードは以下になります.

OSTYPEとOSの関係は,0の場合は"Windows",1の場合は"Linux",2の場合は"Mac OS X"になります.

実行結果は以下になります.

gccの-DオプションでOSTYPEマクロを0に設定するためには,-DOSTYPE=0と書きます.

OSTYPEの値によりOSの文字列の出力結果が異なることがわかります.

また,-DOSTYPE=3と設定すると,14行目の#errorが実行されてコンパイルエラーになります.

このように#elseと#errorを組み合わせると,不正な値を定義した場合のエラーチェックに役立ちます.

※if文,else if文のあとに続くelse文のような使い方です.

ここで,OSTYPEを未定義の場合にどうなるのか実行してみましょう.

OS = Windowsと出力されました.

これは,C言語の仕様では,#ifで未定義マクロを参照した場合,0として評価します.

つまり,OSTYPEを0として評価したので,7行目の#ifが真になり,8行目でOSを"Windows"と定義しました.

未定義なのに正常にコンパイルできてしまうと不正な動作をしてしまうかもしれないので,不安ですよね.

そこで,未定義マクロを参照していることを発見するためには,コンパイルオプションで-Wundefを設定します.

-Wundefオプションを付けてコンパイルすると下記のようにOSTYPEが未定義という警告が発生していることがわかります.

また,7行目のOSTYPE == 0をOSTYPE == 10(0~2以外の値)に変更した後にコンパイルするとエラーになることがわかります.

#if defined,#ifdef:定義済みマクロかどうか判定

トークンが定義されているかどうか判定したい場合には,#if + definedと#ifdefを利用します.

#if definedのコード

#if + definedを利用したコードは以下になります.

前述のOSの名前を判定する場合を修正したものです.

実行結果は以下になります.

-Dオプションの使い方が,-DWINDOWSのように少し変更しているので注意して下さい.

WINDOWS,LINUX,MACOSXトークンのいずれも未定義の場合は,コンパイルエラーになります.

#ifのみの利用する場合(definedを利用しない場合)と比較すると,定義し忘れやタイポを発見できるメリットがあります.

防御的プログラミングの観点では,#if + definedの方が推奨されます.

#ifdefのコード

#ifdefでもトークンが定義されているかどうか判定できますが,#elif definedと同じ意味の予約語がないことに注意して下さい.(いわゆる#elifdefはありません.)

なので,#elifでも利用できる#if + definedの方が柔軟性が高いです.

もちろん#ifdef + #elif definedと書くことはできますが,あまり使いません.

#ifdefは通常は#elifが不要な場合に利用します.

#ifdefを使うメリットは,#if + definedより文字数が少ないのでスッキリ書けることです.

#ifdefのコードは以下になります.

実行結果は以下になります.

OSTYPEトークンが未定義の場合はコンパイルエラーになります.

#ifndef,#define,#endif,#pragma once:インクルードガード

ヘッダファイル(*.h)でよく利用されるインクルードガードを紹介します.

インクルードガードとは,ヘッダファイルを多重にインクルードして,コンパイルエラーになることを防ぐ手法です.

インクルードガードは以下の2種類が使われますので,それぞれ説明していきます.

  • #ifndef,#define,#endif
  • #pragma once

#ifndef,#define,#endifによるインクルードガード

#ifndef,#define,#endifによるインクルードガードは,古典的によく利用される手法です.

あなたがよく利用するstdio.hファイルにも利用されています.

stdio.hの中身を見てみましょう.

以下のように#ifndef,#define,#endifが利用されていることがわかります.

インクルードガードの仕組みを説明します.

まず,stdio.hファイルを読み込む時に,_STDIO_Hは未定義とします.

#ifndefで_STDIO_Hトークンが未定義なので真になり(5行目),#defineで_STDIO_Hトークンを定義し(6行目),#endifでヘッダファイルの中身を挟みます(10行目).

次に,stdio.hもう一度読み込もうとする場合,_STDIO_Hは定義済みなので#ifndefは偽になります.

このようにヘッダファイルを1度しか読まない仕組みを作るのがインクルードガードです.

#ifndef,#define,#endifによるインクルードガードのヘッダとコードは以下になります.

実行結果は以下になります.

#pragma onceによるインクルードガード

#pragma onceによるインクルードガードを紹介します.

ヘッダファイルの先頭に#pragma onceと書くだけでお手軽に利用できます.

#pragma onceによるヘッダとコードは以下になります.

ヘッダは#pragma onceを利用した方が短く書けることがわかります(行数が12行から9行に削減).

実行結果は以下になります.

#演算子:文字列化

文字列化する#演算子を紹介します.

マクロ関数の引数に演算子を#記号をつけて定義することで,文字列(すなわちダブルクォーテーション「"」で挟んだ状態)に置換します.

#演算子は,以下のように書きます.

※stringifyは「文字列化する」という意味の英語です.

#演算子を利用したコードは以下になります.

実行結果は以下になります.

PRINTFマクロ関数はHello World!をダブルクォーテーションで挟んでいませんが,正常にコンパイルして実行できていることがわかります.

#演算子による置換は,文字列内にダブルクォーテーション「"」が頻繁に出てくる場合に便利です.

文字列内で「"」を出力したい場合は毎回「\"」と書かなければなりませんが,#演算子を利用すれば必要はありません.

文字列内に「"」があるコードは以下になります.

実行結果は以下になります.

上記のコードのPRINTFマクロ関数(11行目)とprintf関数(12行目)は,下記の3~4行目に示すように,同じ出力結果になります.

##演算子:トークンの連結

トークンを連結する##演算子を紹介します.

トークンを連結するとは,名前(例:変数名)を指定するのに2つの文字をくっつけて1つの文字に置換することです.

上記のトークンを連結するために「左辺 ## 右辺」と##演算子を利用します.

例えば,「c ## 11」と指定すると,c11に置換されます.

##演算子を利用したコードは以下になります.

実行結果は以下になります.

gccで-Eオプションを付けてコンパイルすると以下のようにプリプロセッサ処理後のコードが読めます.

##演算子を利用した場合,printf関数の中身が正しく展開できていることがわかります.

#if 0を利用したコメントの入れ子

C言語の文法にある/* */のコメントは入れ子できませんでした.

例えば,以下のようにfunc関数を定義した場合,コンパイルエラーになります.

そこで,#if 0は真偽値が必ず偽になることを利用して,#endifまでをコンパイル時に無視できます.

この機能を応用するとコメントを入れ子できます.

#if 0による入れ子したコメントは以下になります.

#if 0によるコメントは便利なので,是非使いましょう!

__FILE__,__func__,__LINE__:C言語の組込みマクロ

C言語の組込みマクロとして,以下の3つをよく使います.

  • __FILE__:ファイル名
  • __func__:関数名
  • __LINE__:行番号

デバッグの時に利用してみましょう.

__FILE__,__func__,__LINE__を利用したコードは以下になります.

実行結果は以下になります.

9行目のprintf関数で,embedded_macro.cファイル,main関数,ファイルの9行目という情報が正しく出力されていることがわかります.

printfデバッグ

あなたがデバッグする時によく利用するprintfデバッグを紹介します.

printfデバッグは,本記事で学んだ以下の内容の集大成になります.

printfデバッグのヘッダとコードは以下になります.

実行結果は以下になります.

-DDEEBUGオプションの有無で,printfデバッグによる変数aの値の出力が変化することがわかります.

私がよく利用するprintfデバッグですので,是非利用してC言語の開発を効率化しましょう.

もちろん,上記のコードを応用してあなただけのprintfデバッグを開発しても構いません.

また,printfデバッグだけでなく,GDBも利用して開発を効率化したいあなたは,【無料】元東大教員がおすすめするC言語の開発ツール2選をあわせて読むことをおすすめします.

C言語 Make
【C言語】Makeの使い方

こういった悩みにお答えします. こういった私が解説していきます. C言語はPython,Ruby,HTML/CSS/JS/PHPのようなアプリケーション系の言語と比較して難易度が高いですよね. 私がC ...

続きを見る

まとめ

C言語のプリプロセッサ(マクロ)の使い方を紹介しました.

本記事に書いてある内容がわかれば,C言語のマクロを神レベルで使いこなせます.

一度読んでわからないところがあれば,何度も読んでマクロを習得しましょう.

C言語を独学で習得することは難しいです.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

友だち追加

独学が難しいあなたは,C言語を学べるおすすめのオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

-C LANGUAGE, TECHNOLOGY
-, , , , , ,