はじめに
- これまでの学習に続いて、今回はdefineを用いたマクロ処理について学んだ結果をまとめてみました。
学習環境
- 今回はpaiza.ioのC言語のエディタを使いました。
プリプロセッサ
- プリプロセッサとは、コンパイラが動作する前にソースコードに対して前処理を行うソフトウェアのことです。
- 例えばソースコード中のコメントは、プリプロセッサによる前処理で削除されます。
- また、ソースコードの先頭に書いてきた
#include
は、プリプロセッサに対して前処理を要求する代表的な命令です。- プリプロセッサが
#include
で指定されたファイルを探しだし、#include
が書かれた部分をヘッダファイルの内容に置き換えます。
- プリプロセッサが
- このような
#define
による文字列の置換は「マクロ定数」と呼ばれ、C言語では広く使われていたそうです。
defineのおさらい
ここは前回の記事からの転載です。
-
#define
は#include
と同様に、プリプロセッサの代表的な命令で、「ソースコード内のある文字列を別の文字列に置換する」という機能を持っています。- この置換処理のことを「展開」と呼ぶそうです。
- 以下のコードでは、プリプロセッサによって「PIが3.1415に置き換えられる」という処理が行われます。
Main.c
#include <stdio.h>
#define PI 3.1415
int main(void){
int r = 2;
printf("半径%dの円の面積:%f\n", r, r * r * PI);
return 0;
}
実行結果
半径2の円の面積:12.566000
マクロの副作用
-
#define
を使ったマクロ定数には以下の副作用があるため、利用する際には注意が必要になるそうです。
副作用1:型や構文のチェックが効かない
-
#define
は単純に文字列の置換だけを行うため、型や構文のチェックが行われません。 - 上記のコードでは
#define PI 3.1415
と記述していますが、以下のように数値として扱われない誤った形式で記述してしまうと、マクロとして定義した箇所では無く、展開される箇所でエラーとなります。
Main.c(誤ったマクロの定義)
#include <stdio.h>
#define PI "3.1415" // ★数値として扱われない形式で記述
int main(void){
int r = 2;
printf("半径%dの円の面積:%f\n", r, r * r * PI);
}
コンパイルエラーの内容
Main.c:7:55: error: invalid operands to binary expression ('int' and 'char [7]')
printf("半径%dの円の面積:%f\n", r, r * r * PI);
~~~~~ ^ ~~
1 error generated.
副作用2:危険な定数展開をしてしまうリスクがある
-
#define
は単純な文字列の置換のため、int
などの予約語を別のものに置換するなど、危険な展開をしてしまうことも可能だそうです。
マクロの副作用を防ぐには?
-
#define
でなければ実現できない特殊事例を除いて、定数として用いる場合はconst
やenum
を使う方が良いそうです。
Main.c(改善版のコード)
#include <stdio.h>
const double PI = 3.1415;
int main(void){
int r = 2;
printf("半径%dの円の面積:%f\n", r, r * r * PI);
}
定義済みマクロ定数
- 以下は、C言語で自動的に定義される(元から定義されている?)マクロ定数で、デバッグ時などに用いられるそうです。
-
__func__
については、厳密にはマクロではないそうです...
-
__FILE__ // このマクロが書かれているソースファイル名
__LINE__ // このマクロが記述された位置(行番号)
__DATE__ // プリプロセッサが起動された日付
__TIME__ // プリプロセッサが起動された時刻
__func__ // このマクロが書かれている関数名
- 以下は
__LINE__
を使った例ですが、このように行番号を標準出力できるとデバッグ時に役立ちそうだと感じました。
Main.c
#include <stdio.h>
int main(void){
printf("ここは%d行目です。\n", __LINE__);
}
実行結果
ここは4行目です。
マクロ関数
-
#define
では以下のように「引数を伴った文字列置換」も出来るため、「関数のようなマクロ」を定義することもできます。 - マクロ関数はマクロ定数と同様の副作用もありますが、さらに「関数展開による予期しない動作のリスク」もあるそうです。
- 処理をマクロ関数として定義するのは、あまり行儀の良い書き方では無いのかもしれませんね...
Main.c
#include <stdio.h>
// #define 置換前の文字列(引数1, 引数2,...) 置換後の文字列
#define ADD(X,Y) X+Y
int main(void){
printf("%d\n", ADD(2,3));
}
実行結果
5
マクロを使うべきか?
- 上記のようにマクロには様々なリスクがある上、通常の定数や関数で安全に置き換えられるシーンが多いと思われます。
- それにも関わらずマクロが使われていたのは、CPUやメモリを節約できるというメリットがあったからだそうです。
- マシンの性能が向上し、さらにコンパイラの最適化が備わっている現代では、マクロをあえて使うシーンは減っていると思われますが...
- レガシーなコードにはマクロが使われていることもあるとおもうので、マクロ定数やマクロ関数については押さえておく必要がありそうです。
補足:これまでの学習の歩み
- 【C】初めてのC言語(1. Hello world)
- 【C】初めてのC言語(2. 四則演算とフォーマット指定子)
- 【C】初めてのC言語(3. ポインタ)
- 【C】初めてのC言語(4. 構造体)
- 【C】初めてのC言語(5. 関数)
- 【C】初めてのC言語(6. 配列)
- 【C】初めてのC言語(7. for文)
- 【C】初めてのC言語(8. 2次元配列)
- 【C】初めてのC言語(9. 関数のプロトタイプ宣言)
- 【C】初めてのC言語(10. 構造体配列)
- 【C】初めてのC言語(11. ヘッダファイル)
- 【C】初めてのC言語(12. 3つのからくり構文+ポインタ演算)
- 【C】初めてのC言語(13. メモリを扱う標準関数)
- 【C】初めてのC言語(14. ヒープの利用)
- 【C】初めてのC言語(15. sizeofの注意点)
- 【C】初めてのC言語(16. 文字列の基本)
- 【C】初めてのC言語(17. 文字列リテラル)
- 【C】初めてのC言語(18. 文字列の受け渡し)
- 【C】初めてのC言語(19. 文字列とバッファオーバーフロー)
- 【C】初めてのC言語(20. 文字列を扱う標準関数)
- 【C】初めてのC言語(21. コマンドライン引数と二重ポインタ)
参考URL
-
スッキリわかるC言語入門 第2版
- p.499~p.504