わたしの知らなかった#defineの世界
C言語の#define
はプログラマーが定義した文字列を他の文字列(や数値、式等)に置き換えるものです。
その活用方法は主に、以下があります。
・マジックナンバーの防止
・関数定義
・多重インクルード防止
・条件付きコンパイル
マジックナンバーの防止
まず、一番基本的な使い方がこれになります。
#include <stdio.h> #define MAX_VALUE 100 #define ARRAY_SIZE 10 int main() { int numbers[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { numbers[i] = i * 2; } for (int i = 0; i < ARRAY_SIZE; i++) { if (numbers[i] > MAX_VALUE) { printf("Number %d exceeds the maximum value.\n", numbers[i]); } } return 0; }
上記の例では、MAX_VALUE
とARRAY_SIZE
という定数を #define
ディレクティブを使用して定義しています。これにより、ハードコーディングされた数値を避け、数値が意味するものが明確になります。
マジックナンバーを定数として定義することで、コードの可読性が向上します。
#define
使用して定数を定義する方法に対して、例えばint変数として定数を宣言する方法があります。
const int MAX_VALUE = 100; const int ARRAY_SIZE = 10;
これでもコードとしては同じ動きをします。
しかしながら、#define
使用して定数を定義する方法とint変数として定数を宣言する方法には以下のような違いがあります。
#define
を使用すると、コンパイル前にプリプロセッサによってマクロが置換されます。つまり#define
で定義された定数はプリプロセッサによって単なるテキスト置換が行われるため、実際のコード中では定数の値が直接埋め込まれます。一方、int
変数として定数を宣言する場合、変数自体がメモリを占有し、実行時にそのメモリ領域に値が格納されます。
関数定義
#define
を使用して関数を定義する場合、マクロを使用して関数の本体を展開します。ただし、通常の関数とは厳密には異なる動作をします。
#include <stdio.h> #define ADD(a, b) ((a) + (b)) // 加算関数の定義 int main() { int x = 5; int y = 3; int result = ADD(x, y); // マクロを呼び出して加算を行う printf("Result: %d\n", result); return 0; }
このようにすると、あたかも
int ADD(x,y) { return x + y; }
のような関数が定義されているかのような動きをします。
しかし、ここでのADD(x, y)
の呼び出しは、((x) + (y))
というテキスト置換が行われるため、コンパイル時に (x) + (y)
の式が展開されます。このように、マクロはソースコード中のテキストを置換するだけであり、実行時に関数の呼び出しやスタックの操作は行われません。
マクロを使用して関数を定義する場合には注意点があります。マクロは関数の呼び出しではなくテキストの置換なので、引数や戻り値の型の検査が行われません。また、マクロ展開中に副作用が発生する場合や、引数の値を複数回評価する場合には意図しない結果が生じる可能性があります。
このようなデメリットがあるのになぜ使われるのか?をChatGPTに聞いてみたところ、以下のような回答を得ました。
柔軟なマクロ展開: マクロを使用して関数を定義すると、ソースコードの特定の箇所でマクロを展開することができます。これにより、関数呼び出しのオーバーヘッドが発生せず、関数の本体が直接展開されるため、実行時のパフォーマンスが向上します。
引数の柔軟な処理: マクロでは引数をテキスト置換するため、引数の評価や操作に制約がありません。これにより、任意の式や複数の引数を使用して関数本体を展開することができます。例えば、引数の型によって異なる処理を行いたい場合など、柔軟な処理が可能です。
プログラムの柔軟性: マクロを使用して関数を定義することで、コンパイル時の条件に応じて関数の振る舞いを変更することができます。例えば、特定のプラットフォームや環境で異なる実装を必要とする場合、条件付きコンパイルによって関数の定義を切り替えることができます。
短くてシンプルなコード: マクロを使用して関数を定義することで、コードが短くてシンプルになります。関数呼び出しのオーバーヘッドがなくなるため、パフォーマンスが向上するだけでなく、ソースコードの行数や冗長性を削減することもできます。
また、この関数定義の応用になるかと思いますが、以下の様な定義をするとprintf()
を無効化することができます。
#define printf(...)
は(...)
を使うことでprintf()
の引数を含めて#define
の左辺とし、これをコンパイル時に""
に変換することで無効にします。
#include <stdio.h> #define printf(...) int main(void){ printf("1\n"); printf("%d\n",1+2); return 0; } /* 何も出力されない*/
多重インクルード防止
#include
は標準モジュールや自作のモジュール(ファイル)を読み込むものです。
読み込まれたファイルには、関数や変数が定義されています。
ここで、複数のソースファイルから一つのモジュールを読み込むとき、規模の大きなプロダクトになると、読み込んだファイルからさらにファイルを読み込む、ということが発生します。
すると、関数や変数の定義が複数回行われることになります。
これを防止するために、一度読み込まれたファイルは読み込まないようにするのが、多重インクルードの防止です。
例えば、以下のリポジトリでは、images.hpp
というファイルが複数回インクルードされています。
C言語において、ヘッダーファイルが多重にインクルードされるとコンパイラエラーが発生します。一般的には以下のようなエラーメッセージが表示されます
error: redefinition of 'symbol_name'
これを防止するには、以下のようにIMAGES_HPP
という定義値を使い、これが定義されていない = このファイルが読み込まれていない、と判断できるようにします。
これをヘッダーガード(header guards)といい、このdefineでファイル全体を囲みます。
GitHub - shapoco/pico-env-mon: environment monitor
条件付きコンパイル
条件付きコンパイルは、#ifdef
、#ifndef
、#else
、#endif
などのプリプロセッサディレクティブを使用して、特定の条件が満たされた場合にのみコードをコンパイルすることができる機能です。以下のようなものです。
#include <stdio.h> #define DEBUG_ENABLED // デバッグモードを有効化するフラグ int main() { #ifdef DEBUG_ENABLED printf("Debug mode is enabled.\n"); #else printf("Debug mode is disabled.\n"); #endif // コードの実行 return 0; }
上記の例ではDEBUG_ENABLED
というマクロを定義しています。このマクロが定義されている場合、#ifdef DEBUG_ENABLED
の条件が満たされ、その下のコードが有効化されます。定義されていない場合は、#ifdef
ブロック内のコードは無視されます。
上記の例では、デバッグモードを有効化するために DEBUG_ENABLED
マクロを定義しています。デバッグモードが有効化されている場合、printf ステートメントは "Debug mode is enabled." を出力します。デバッグモードが無効化されている場合、printf ステートメントは "Debug mode is disabled." を出力します。
条件付きコンパイルは、特定のコンパイルフラグやプラットフォームに依存するコードの有効化や無効化、異なるビルド設定の切り替えなどに利用されます。これにより、コンパイル時のオプションに応じてコードの振る舞いを変更することができます。
通常のif
による条件訳でも機能を満たすことができる場合も多くありますが、#ifdef
を使うと、その有効でない範囲はコンパイルされない(実質のコード量が少なくなり、バイナリも小さくなる)という違いがあります。
まとめ
C言語の#define
の使い方をまとめました。