こんな人にオススメ
C言語の#define
って何?これを使用する意味ってあるの?
ということで、今回はC言語のコードの最初の方に書かれる#define
について解説する。個人的にこれって意味あるの?と思っていた瞬間があったが、その存在意義を知ったことで意味しかないとなった。
執筆者のようにいまいち使い所がわからんという方も本記事を参考にしていただいて納得することがあれば幸いだ。
今回も猫Cこと「猫でもわかるC言語プログラミング 第3版」を参考にした。今回は11.7の「プリプロセッサ」を中心に参考とさせていただきました。


#define
とは具体的には以下コードの上の方に書かれている文言。コードでは円の面積を計算しているが、この時の円周率$\pi$を#define
を使用して定義している。
#include <stdio.h> #define PI 3.14159265 int main(void) { int r = 5; printf("半径が%dの円の面積は%fです。\\n", r, r * r * PI); return 0; }
このコードだけ見て、いや今まで通り普通に宣言するだけでいいやんと思っていた。しかし、これが真価を発揮するのは複数ファイルになった時。
前置きはこれくらいにして早速解説に移る。
環境設定について何を書けばいいのかわからないので、Macのバージョンとgccのバージョンを示す。
- macOS Big Surバージョン11.4
- M1 Macbook Pro(2020)
- Apple clang version 12.0.5 (clang-1205.0.22.9)
目次(クリック・タップでジャンプ)
プリプロセッサ
冒頭で書いたコードで、#define
はいつものようにmain
の中に書いていない(一応、書いてもいいらしいが)。代わりに初めの#include <stdio.h>
の真下に書かれている。
この初めの部分はコンパイラが翻訳を始める前に処理される部分で、この処理を行うプログラムのことを「プリプロセッサ」(preprocessor)と呼ぶ。
プロセスの前段階を担うということで、プリ・プロセッサだ。
ヘッダファイル
で、ここで書かれている#include <stdio.h>は
ヘッダファイル(.h
形式)を読み込む処理を表す。ここではstdio.h
というヘッダファイルをPC内から探してきている。
ちなみにstdio
は「Standard Input/Output」の略称。これを知った時は世界が開けた。
これらの他にもmath.h
やstdlib.h
などがあり、includeすることでそれぞれ数学系の関数(累乗とか)、一般的な関数(乱数生成とか)を使用することができる。
なお、自作のヘッダファイルの時は#include "jisaku.h"
のように書く。<>
は使わず""
を使う。
#define
ここまでは#include
だったが、#defineのこの部分で記述する。書き方は
#define マクロ名 文字列
という具合。マクロ名とかピンと来ない場合はイメージとしては以下のイメージ。本当は違うからあくまでもイメージで。
#define 変数名 その値
冒頭の例だと
#define PI 3.14159265
という記述だった。慣習としてマクロ名は全て大文字で書く。で、実際には
printf("半径が%dの円の面積は%fです。\\n", r, r * r * PI);
として使用されていたのだが、ここでの注意点は、
文字列を「代入」ではなく「置換」する
ということ。ただただシンプルにコード上で「PI
」と書かれた部分を該当する値(今では3.14159265
)に置き換えるだけ。
すなわち、3.14159265
と書くところをPI
としておきますよ、というだけ。int pi = 3.14159265
のように変数として扱うことではない。
それでは次章より、実際にコードを書いて実行してみる。
#define
で定数を置き換え
まずは冒頭のコードである、円周率を#define
で置き換えというコード。さらに、#include
で自作のヘッダファイルを読み込んで円周率を引用する方法についても解説する。
同じファイル内で#define
同じファイル内で#define
した場合が冒頭のコード。
#include <stdio.h> #define PI 3.14159265 int main(void) { int r = 5; printf("半径が%dの円の面積は%fです。\\n", r, r * r * PI); return 0; } /* 半径が5の円の面積は78.539816です。 */
コード初めにPIを定義し、main
内でPI
を使用している。これだけのコードではint r = 5;
のように書いてもいいが、PI
を定義することで以下のメリットがある。
3.14159265
という数値は円周率$\pi$(PI
)であることが明示的3.1415
に修正する場合にPI
だけ修正すればいい
これだけだとやはりint pi = 3.14159265;
としても達成できそう。だが、次節のことがあるので#define
は便利。
自作ヘッダファイルを読み込み
以下に今回作成した自作のヘッダファイル「c_jisaku.h
」を記述する。今は1文だけだが、後のコードではそれに応じて中身を増やしている。
#define PI 3.141592654
このヘッダファイルを読み込んで、円の面積を計算するコードを書いてみる。
#include <stdio.h> #include "c_jisaku.h" int main(void) { int r = 5; printf("半径が%dの円の面積は%fです。\\n", r, r * r * PI); return 0; } /* 半径が5の円の面積は78.539816です。 */
まあ、出力は同じだ。そして別ファイルにPIが移動しただけだ。しかし、もしこのPIを他のファイルでも使用したい場合はどうだろうか。例えば以下のコード。このコードは半径を10にしつつ文言を変えた。
#include <stdio.h> #include "c_jisaku.h" int main(void) { int r = 10; printf("ここでは円の面積を求めるよ\\n"); printf("半径が%dの円の面積は%f\\n", r, r * r * PI); return 0; } /* ここでは円の面積を求めるよ 半径が10の円の面積は78.539816 */
どちらも同じヘッダファイルc_jisaku.h
を読み込んでプログラムを実行している。で、もし、PI
の値を変更しないといけない場合、c_jisaku.h
だけを変更すればいいのだ。
もしそれぞれのファイルで#define PI 3.14159265
としていた場合、それぞれのファイルでPI
の値を変更しないといけない。かなり面倒。
しかし、ヘッダファイルに共通事項として載せておくだけで一箇所の変更で事足りる。pythonではよくしているのになぜ気づかなかった。
この便利さを知ってからは頭がスッキリした。結論、
「コードの可読性(読みやすさ)と保守性(改変のしやすさ)を向上する」
ために#define
を使用する。
#define
で関数も置き換え
ではここからは#define
を使用して色々なコードを書いてみる。
関数を#define
で読み込み
前章ではPIという一つの数値を置き換えていたが、ここでは関数形式を採用する。使用するヘッダファイルc_jisaku.h
は以下。
#define PI 3.141592654 #define CIRCLE_AREA(x) (x * x * PI)
新たにCIRCLE_AREAというマクロを定義した。これは引数としてxをとり、そのxとPIの値より半径xの円の面積を計算するというもの。
このヘッダファイルを読み込んで円の面積を計算したコードが以下。
#include <stdio.h> #include "c_jisaku.h" int main(void) { int r = 5; printf("半径が%dの円の面積は%fです。\\n", r, r * r * PI); printf("半径が%dの円の面積は%fです。\\n", r, CIRCLE_AREA(r)); return 0; } /* 半径が5の円の面積は78.539816です。 半径が5の円の面積は78.539816です。 */
どちらも同じ結果になる。上のprintf
の計算は前章の計算と同じで、PI
を用いてその場で円の面積を計算している。
一方で下のprintf
の計算では、半径r
をCIRCLE_AREA
の引数として代入して円の面積を計算している。計算の中身はヘッダファイルに書いている。
既に述べたように、仮に他のファイルでもCIRCLE_AREA
を使用することになった場合でもc_jisaku.h
で関数を修正するだけで大丈夫。これは#define
強い。
掛け算の入った#define
では定義方法に注意
ではもう少し複雑な関数を用いてみる。ヘッダファイルc_jisaku.h
は以下に変更。
#define PI 3.141592654 #define CIRCLE_AREA(x) (x * x * PI) #define KAKEZAN(x, y) (x * y)
ここではKAKEZAN
という引数x
, y
を掛け算するという関数を新たに入れた。この関数を使用してプログラムを組んでみる。
#include <stdio.h> #include "c_jisaku.h" int main(void) { int a = 2, b = 3, c = 4, d = 5, e1, e2; // 予想では(2 + 3) * (4 + 5) = 45 e1 = KAKEZAN(a + b, c + d); printf("カッコなし%d\\n", e1); // 実際には2 + 3 * 4 + 5 = 2 + 12 + 5 = 19 // カッコをつければ先に要素同士の計算が行われる e2 = KAKEZAN((a + b), (c + d)); printf("カッコなしカッコなし%d\\n", e2); // (2 + 3) * (4 + 5) = 5 * 9 = 45 return 0; } /* カッコなし19 カッコあり45 */
このコードではKAKEZAN
の引数にx
にa
, b
を、y
にc
, d
を入れて掛け算している。2種類の引数の入れ方を試しているが、それぞれで計算結果が異なる。
先ほども書いたように#define
では単純に「置換」するだけなのでカッコなしのe1
の場合は以下の式が適用される。
$$\begin{align*} x\ \times\ y\ &=\ a\ +\ b\ \times\ c\ +\ d\\ &=\ 2\ +\ 3\ \times\ 4\ +\ 5\\ &=\ 2\ +\ 12\ +\ 5\\ &=\ 19 \end{align*}$$
とすると、掛け算の方が先に計算されるので結果は19
となる。この問題の回避方法は至ってシンプル。引数をカッコでくくればいい。それがe2
。この場合は以下の計算となる。
$$\begin{align*} x\ \times\ y\ &=\ (a\ +\ b)\ \times\ (c\ +\ d)\\ &=\ (2\ +\ 3)\ \times\ (4\ +\ 5)\\ &=\ 5\ \times\ 9\\ &=\ 45 \end{align*}$$
これで予想通りの計算ができた。この計算の思わぬ結果は「マクロの副作用」というらしい。
いや、そもそもヘッダファイルでカッコつければよくね?
そもそも論でヘッダファイルにカッコをつければいいのでは?という疑問だがそれは正しい。ヘッダファイルc_hisaku.h
を以下のように変更。
#define PI 3.141592654 #define CIRCLE_AREA(x) (x * x * PI) #define KAKEZAN(x, y) (x * y) #define KAKEZAN2(x, y) ((x) * (y))
この場合はx
, y
どちらの引数にもカッコが着くのでマクロの副作用は発生しない。
#include <stdio.h> #include "c_jisaku.h" int main(void) { int a = 2, b = 3, c = 4, d = 5, e1, e2; // defineの時点でカッコがつけられているので、ここでカッコをつけなくても大丈夫 e1 = KAKEZAN2(a + b, c + d); printf("カッコなし%d\\n", e1); // もちろんカッコをつけても大丈夫 e2 = KAKEZAN2((a + b), (c + d)); printf("カッコなし%d\\n", e2); return 0; } /* 19 45 */
副作用を起こしなくない時は予め#define
でカッコを使用するのがいいだろう。
#define
では型は決めない
ここまでで#define
とヘッダファイルについて解説したが、ここで気になることがある。
「#define
で定義した関数などには型はないのか」
確かに以下のように型を定義していない。
#define PI 3.141592654 #define CIRCLE_AREA(x) (x * x * PI) #define KAKEZAN2(x, y) ((x) * (y))
というのも#define
はあくまでも「置換」。したがって、その時々で型を決まる。
以下の例ではPI
をprintf
時に10
進数表記と小数表記にしている。また、KAKEZAN
の引数を整数と小数にしている。
#include <stdio.h> #include "c_jisaku.h" int main(void) { int i = 10, j = 20; double m = 10., n = 20.; printf("%%d, %%f: %d, %f\\n", PI, PI); printf("----------\\n"); printf("i, j: %d, %d\\n", i, j); printf("m, n: %f, %f\\n", m, n); printf("KAKEZAN(i, j): %d\\n", KAKEZAN(i, j)); printf("KAKEZAN(m, n): %f\\n", KAKEZAN(m, n)); printf("KAKEZAN(i, n): %f\\n", KAKEZAN(i, n)); return 0; } /* c_define4.c:9:34: warning: format specifies type 'int' but the argument has type 'double' [-Wformat] printf("%%d, %%f: %d, %f\\n", PI, PI); ~~ ^~ %f ./c_jisaku.h:1:12: note: expanded from macro 'PI' #define PI 3.141592654 ^~~~~~~~~~~ 1 warning generated. %d, %f: 74585080, 3.141593 ---------- i, j: 10, 20 m, n: 10.000000, 20.000000 KAKEZAN(i, j): 200 KAKEZAN(m, n): 200.000000 KAKEZAN(i, n): 200.000000 */
そうすると、まずはPI
はもともと小数系だったので%d
では出力がおかしくなるが、%f
では正常に出力される。
さらに、KAKEZAN
では整数同士、小数同士、整数と小数の合計3種類の計算を行った。そうすると
KAKEZAN(i, j)
:%d
KAKEZAN(m, n)
:%f
KAKEZAN(i, n)
:%f
で何も警告もなしで出力できた。なお、一番上のKAKEZAN(i, j)
で%f
にすれば出力はできるが警告が出る。
すなわち、引数による計算処理の結果で出力の型が決まる。あくまでも置換で型は計算時に決定するイメージだろう。
#define
で文字を置き換え
最後は簡単に文字の扱いを解説する。前章まででは数値について行ってきたが、もちろん文字についても#define
することができる。ヘッダファイルc_jisaku.h
は以下のようにした。
#define PI 3.141592654 #define CIRCLE_AREA(x) (x * x * PI) #define KAKEZAN(x, y) (x * y) #define KAKEZAN2(x, y) ((x) * (y)) #define STRING 'Z' #define STRINGS "ABC"
今回は文字として'Z'
を、文字列として"ABC"
を定義した。これを用いたコードが以下。
#include <stdio.h> #include "c_jisaku.h" int main(void) { printf("STRING%%c: %c\\n", STRING); printf("STRINGS%%s: %s\\n", STRINGS); return 0; } /* STRING%c: Z STRINGS%s: ABC */
それぞれ出力することができている。エラー時に出力する文章などをヘッダファイルで一括管理することが可能になる。
可読性と保守性を向上させるために
今回はC言語の#define
について解説した。実は#define
の親戚で#ifdef
, #enddef
なども存在している。ここら辺については今回は触れなかったが、こちらもif
がついて同じような感じ。
ヘッダファイルで#define
を使用して値を定義することで、変数が何を意味するのかわかりやすくなり、そして迅速・正確にプログラムの修正を行うことができる。それぞれ可読性と保守性だ。
まだチームで開発するという経験がないが、その未来のためにも#define
の働きと可読性、保守性について理解が必要だ。


関連記事
-
【C言語&構造体】複数のデータ型を一つに格納したい(ポインタなし)
2021/12/11
-
【C言語&文字列操作】strlenなどのCの文字列操作を試す
2021/11/20
-
【C言語&define】C言語の#defineを調べコードを書いてみた
2021/11/20
-
【C言語&switch文】C言語初心者がswitch文を学ぶ
2021/11/20