8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

C言語のマクロについてまとめる

Last updated at Posted at 2023-08-31

マクロとは

C言語などにおけるマクロとは,プログラム内の文字列をあらかじめ定義されている規則にしたがって置換する機能のことをいい,これをマクロ置換と呼びます.マクロには固有のデータ型がなく,コンパイラによる型の整合性の確認が行われません
マクロは,#defineというプリプロセッサ指令により定義します.

プリプロセッサとは,コンパイルに先立って行われるプリプロセス(前処理)を行うプログラムのことで,それに対する指令がプリプロセッサ指令で,前処理指令ディレクティブとも呼ばれます.

マクロには,プログラム中の文字列を単に指定した文字列に変換するオブジェクト形式マクロと,引数を用いて関数のようにふるまう関数形式マクロの2種類があります.

オブジェクト形式マクロ

オブジェクト形式マクロはプログラム内の文字列を指定した文字列に変換するもので,以下のように定義します.
#define 文字列1 文字列2
このように定義することでプログラム内の文字列1を文字列2に置換することができ,文字列2は省略することもできます.
よく使われる例として,#define NUM 10などとして配列の要素数を指定したり,定数を定義したりします.定数に置換するマクロ名は,一般に全て大文字で記述します.
このマクロを利用するメリットとして,

  • マクロの定義部分だけを変更すればすべてに反映されるため,保守性が向上して修正がしやすいこと.
  • 単なる値としてではなく,文字列に置換することで可読性が向上すること.

などがあります.

関数形式マクロ

関数形式マクロは関数のようにふるまうマクロで,次のように定義します.  
#define マクロ名(引数) 処理
関数形式マクロにおいて,引数は型を指定する必要がありません.また,戻り値をreturn句で返す必要もありません.
実際に以下の例をみてみましょう.

関数形式マクロ
#include <stdio.h>   

#define PI 3.14 // オブジェクト形式マクロで円周率を定義
#define area(r) (r*r*PI) // 関数形式マクロで面積計算
#define prt(f) printf("%f\n", f) // 関数形式マクロで標準出力
     
int main(void) {
  prt(area(3.0));

  return 0;
}
実行結果
28.260000

マクロ置換が行われることで,関数呼び出し時のオーバーヘッドがない反面,呼び出しが多いとプログラムのサイズが増加するといった特徴があります.

マクロの応用

ここからはマクロの応用例をいくつか紹介したいと思います.

複数行に分けてマクロを書く

複数行にわたってマクロを定義する場合は,各行の終わりにプリプロセッサの制御命令であるバックスラッシュ「\」を付けて改行します.このとき,バックスラッシュの後には空白やコメントも含め,何も書いてはいけないことに注意してください.
また,可読性やエラー対策として,do-while文を併用することも多いです.以下に例を示します.

複数行のマクロ
#include <stdio.h>

#define MULTI_LINE_MACRO(x, y) \
  do { \
    printf("x:%d\n", x); \
    printf("y:%d\n", y); \
  } while (0)

int main() {
  int a = 10;
  int b = 20;

  MULTI_LINE_MACRO(a, b);

  return 0;
}
実行結果
x:10
y:20

マクロによる分岐

マクロが定義されているかどうかを判断基準にして,プリプロセスにおける分岐を行えます.
if definedを使用した記述方法は以下の通りです.

#if defined(マクロ名)
  マクロが定義されているときの処理
#endif

definedは指定したマクロが定義されていたら1に,定義されていなければ0になります.条件式のマクロが定義されているときだけ処理が実行されます.

ifdefifndefを用いた条件分岐も行えます.

#ifdef マクロ名
  マクロが定義されているときの処理
#endif

#ifndef マクロ名
  マクロが定義されていないときの処理
#endif

上のいずれのディレクティブにおいてもelse句を追加することができます.

分岐の制御のためだけに使うマクロは,置換後の文字列に注目しないので,置換後の文字列を省略して定義することもあります.

インクルードガード

ヘッダファイル内で多数のヘッダファイルをインクルードする際,マクロの定義が重複したり,インクルード先のファイルでインクルード元のファイルをインクルードするといった無限ループが起きてコンパイルエラーとなることがあります.
このような事態を避けるために,先ほどのマクロの分岐を利用したインクルードガードといった仕組みがあります.

sample.h
#ifndef SAMPLE_H
#define SAMPLE_H
// ヘッダファイルの中身
#endif

#defineで定義される識別子には,一般的にヘッダファイルのファイル名を大文字にしたものが使用されます.

プラグマ#pragma onceを使用してインクルードガードを実現することもあります.

マクロの有効範囲

マクロの有効範囲はコンパイル単位となり,C言語の文法ルールの枠外にあります.ここで,コンパイル単位とは,#includeでインクルードしたファイルが展開された後のファイルのことです.そのため,ヘッダファイルにマクロ定義を書いた場合,そのヘッダファイルをインクルードしたすべてのファイルに影響を及ぼします.
また,マクロは#defineで定義された以降の行で有効で,C言語の規格上,同じ名前のオブジェクト形式マクロの定義が2度現れた場合,置換結果がまったく同じであれば問題ないことになっています.置換結果が異なる場合はエラーとある可能性があります.
複数のファイルで使用するマクロはインクルード用ファイルに記述しておき,そのマクロを使用するファイルにおいて,インクルード用ファイルをインクルードして使用するというのが一般的な使い方です.

注意
#define int floatのようなマクロも作成可能であり,有効範囲も広いため,マクロの使用は慎重に行う必要があります.
C++などでは,マクロをできるだけ避けるべきであるといった考え方もありますが,C言語にはconstexpr変数などがないため,マクロを使う必要性が出てくる場面があります.

マクロの無効化

マクロを無効にするには,#undefディレクティブを使います. 存在しないマクロ名を指定した場合は何も起こりません.記述方法は以下の通りです.
#undef マクロ名
マクロの効力は#undefの記述がある部分で無効化されるため,マクロの有効範囲を抑えることができます.

マクロの無効化
#include <stdio.h>

int main(void) {
  // printf("%s¥n", HELLO_WORLD); // コンパイルエラー

  #define HELLO_WORLD "Hello world!"

  printf("%s\n", HELLO_WORLD);

  #undef HELLO_WORLD // マクロの無効化

  // printf("%s¥n", HELLO_WORLD); // コンパイルエラー
  return 0;
}
実行結果
Hello world!

関数形式マクロの計算で気を付けること

関数形式マクロを使用するときは記述方法に気を付けないといけません.
#define TIMES(x) x * 2
上のようなマクロがあった場合,TIMES(3)とすると,3 * 2と展開されて正しく計算できますが,TIMES(2 + 5)として14と計算したくても,2 + 5 * 2と展開され,意図しない答えとなってしまいます.これは,
#define TIMES(x) ((x) * 2)
のように,パラメータに括弧を付けることで解決できます.
このように,マクロはあくまで「文字列の置換」を行うだけなので,マクロで計算を行う際は,できるだけ括弧を付けて,意図した通りに計算を行うようにしておくことが大切です.

文字列化演算子

関数形式マクロのみ使用できる文字列化演算子「#」があります.これはパラメータを文字列に変換するもので,
#define TOSTRING(x) #x
といったマクロがあるとき,
printf("%s\n", TOSTRING(3));
と実行すると,文字列として3が出力されます.
しかし,パラメータに以下のようなマクロを使用するとうまくマクロが展開されません.
#define NAME Taro
このマクロをTOSTRINGマクロに入れてもNAMEと出力されてしまいます.これを解消するには,以下のように2段階でマクロを使用することでうまくマクロを展開できます.

文字列化演算子
#include <stdio.h>

#define TOSTRING(x) #x
#define EXPAND(x) TOSTRING(x) // ここでマクロを展開してからTOSTRINGに代入
#define NAME Taro

int main() {
  printf("%s\n", EXPAND(NAME));

  return 0;
}
実行結果
Taro

この文字列化演算子はデバッグやログ生成などにも役に立ちます.

トークン連結演算子

トークン連結演算子「##」を用いると,2つのトークンを結合して新しいトークンを生成できます.

トークン連結演算子
#include <stdio.h>

#define CONCAT(x, y) x ## y // トークンを連結する

int main() {
    int num10 = 10;
    printf("%d\n", CONCAT(num, 10)); // num10の値が表示される
    return 0;
}
実行結果
10

上のコードでは,トークン連結演算子によってnum10が結合してnum10といったトークンとなり,int型の変数num10の値が出力されます.
トークン連結演算子においても,パラメータにマクロを使用する場合は,先ほどのようにマクロを2段階にして展開する必要があります.

まとめ

今回はC言語のマクロについて整理しました.
定数や関数などを再利用でき,紹介してきたようにとても便利な機能ですが,C言語のルールに準拠せずにプリプロセッサで処理されることや,影響範囲が広いことから,使い方を間違えると意図しない問題が起こってしまいます.
マクロを含め,プログラムがプリプロセッサなどで,どのように実行されているのかを一度探究してみても面白いかもしれませんね✨

8
8
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?