カテゴリー

C言語

【C言語&define】C言語の#defineを調べコードを書いてみた

2021年6月6日

こんな人にオススメ

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)

運営者のメガネとです。YouTubeTwitterInstagramも運営中。

自己紹介はこちらから、お問い合わせはこちらからお願いいたします。

運営者メガネ

プリプロセッサ

冒頭で書いたコードで、#defineはいつものようにmainの中に書いていない(一応、書いてもいいらしいが)。代わりに初めの#include <stdio.h>の真下に書かれている。

この初めの部分はコンパイラが翻訳を始める前に処理される部分で、この処理を行うプログラムのことを「プリプロセッサ」(preprocessor)と呼ぶ。

プロセスの前段階を担うということで、プリ・プロセッサだ。

ヘッダファイル

で、ここで書かれている#include <stdio.h>はヘッダファイル(.h形式)を読み込む処理を表す。ここではstdio.hというヘッダファイルをPC内から探してきている。

ちなみにstdioは「Standard Input/Output」の略称。これを知った時は世界が開けた。

これらの他にもmath.hstdlib.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の計算では、半径rCIRCLE_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の引数にxa, bを、yc, 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はあくまでも「置換」。したがって、その時々で型を決まる。

以下の例ではPIprintf時に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言語

【C言語&構造体】複数のデータ型を一つに格納したい(ポインタなし)

2021/12/11

こんな人にオススメ C言語で ...

C言語

【C言語&文字列操作】strlenなどのCの文字列操作を試す

2021/11/20

  こんな人にオススメ C言語 ...

C言語

【C言語&define】C言語の#defineを調べコードを書いてみた

2021/11/20

こんな人にオススメ C言語の#d ...

C言語

【C言語&switch文】C言語初心者がswitch文を学ぶ

2021/11/20

こんな人にオススメ C言語のsw ...

スイッチボット

2022/9/11

【SwitchBotロックレビュー】これからのスタンダードになりうるスマートロック

こんな人にオススメ SwitchBotからスマートロック「SwitchBotロック」が発売された ...

生活に役立つ

2022/10/25

【メガネ厳選】クソ便利に使っているサービスやアイテム達

このページでは執筆者「メガネ」が実際に使って便利だと感じているサ ...

マウス

2022/9/11

【Logicool MX ERGO vs MX Master 3】ERGOをメインにした決定的な理由

こんな疑問・お悩みを持っている人におすすめ 執筆者はLogicoolのハイエンӠ ...

完全ワイヤレスイヤホン(TWS)

2022/11/21

【ながら聴きイヤホン比較】SONY LinkBuds、ambie、BoCoはどれがおすすめ?

こんな人におすすめ 耳を塞がない開放型のイヤホンに完全ワイヤレスӟ ...

macOSアプリケーション

2022/10/15

【M1 Mac】MacBook Proに入れている便利でニッチなアプリを21個紹介する

こんな人におすすめ MacBookを購入してLINEとか必要最低限のアプリは入れた。 ...

完全ワイヤレスイヤホン(TWS)

2022/10/23

【SENNHEISER MOMENTUM True Wireless 3レビュー】高レベルでバランス型の高音質イヤホン

こんな人におすすめ SENNHEISER MOMENTUM True Wireless 3って実際のところどうなの? 評判は良い ...

完全ワイヤレスイヤホン(TWS)

2022/11/21

【SONY WF-1000XM4レビュー】神とゴミのハーフ&ハーフ

こんな人におすすめ SONYのフラグシップモデル「SONY WF-1000XM4」ってどれくらい性 ...

完全ワイヤレスイヤホン(TWS)

2022/8/19

【Nothing ear (1)レビュー】ライトな完成度、アップデートに期待

こんな人にオススメ 完全ワイヤレスイヤホン(TWS)でスケルトンボディ ...

  • この記事を書いた人

メガネ

ベンチャー企業のWebエンジニア駆け出し。独学のPythonで天文学系の大学院を修了→新卒を1.5年で辞める→転職→今に至る。
常時金欠のガジェット好きでM1 MacBook Pro x Galaxy S22 Ultraの狂人。
人見知りで根暗だったけど、人生楽しもうと思って良い方向に狂う→人生が楽しい

ガジェットのレビューとPythonコードを記事にしています。ぜひ楽しんでください🦊
自己紹介と半生→変わって楽しいの繰り返し

-C言語
-,