こんにちは、ナナです。
プリプロセッサ3大機能のうちの1つdefine機能による「マクロ定義」について解説しましょう。
マクロ定義は「定数」というものに名前を与えるための機能です。名前を作り出すことでどのようなメリットがあるのかを学んでいきましょう。
本記事では次の疑問点を解消する内容となっています。
では、「マクロ定義」機能の使い方を学んでいきましょう。
「プリプロセッサ」が、そもそも何かわからない方は『C言語 プリプロセッサ【絶対知るべき3大機能を順に解説する】』の記事を先に見ておくとよいでしょう。
defineによる「マクロ定義」とはどのような機能なのか?
再び学びに来て差し上げたわよ、わたくしが!ありがたく思いなさいっ。
今日のテーマは…マクロ定義?一体なんですの、それは?
今日紹介する「マクロ定義」機能は、あだ名付けの名人のことだよ。それでは、名人っ!ご登壇お願いします!どーぞーパチパチ。
ふぉ、ふぉ、ふぉ、お呼びですかな。
「マクロ定義」は、数値に名前を付けるための機能です。
皆さんがプログラムの中で扱う値に、任意の名前を与えることができるのです。では、名前を付けることに、いったいどのような便利さがあるのでしょうか?
定数とマジックナンバー
皆さん、次のプログラムの「printPrice関数」が、いったい何をする関数なのかわかりますか?
#include <stdio.h>
void printPrice(int kind)
{
switch (kind)
{
case 0:
printf("250円\n");
break;
case 1:
printf("100円\n");
break;
case 2:
printf("400円\n");
break;
}
}
int main(void)
{
printPrice(2);
return 0;
}
print(印字)price(価格)という関数名から、何かの価格を表示するための関数であろうことは想像できますね。
このプログラムの中で登場する「1」や「2」といった、値を変えられない数値のことを「定数」と呼びます。
では、これらの定数の数に込められた意味がみなさんにはわかりますか?
わかりませんよね。そうは当然です。この数値の意味は私が知っているだけで、皆さんにその数値の意味を教えていないのですから、わかるわけがありません。
このように他の人から見て、意味の分からない定数のことを「マジックナンバー」と呼びます。
マジックナンバーが多用されたプログラムは解析が困難であり、メンテナンス作業が大変になります。プログラムは人が読むものでもあるということを、決して忘れてはいけません。
「マクロ定義」とは、このようなマジックナンバーを防止するために使用する機能です。
一体この数の意味は何なの?マジックナンバーを使った人、わたくしの前に出てきて謝罪なさいっ!
define マクロの定義方法
名前を付ける名人?あぁたそれでよく生活しているわね。じゃあ、わたくしに名前をつけてごらんなさいよっ!
「デヴィ小夫人」
さぁ、名人より素敵なあだ名をいただいたところで、マクロ定義の方法を学びましょう。
マクロ定義の書き方
マジックナンバーに名前を付けることができるのが「マクロ定義」です。マクロ定義は次のように行います。
書き方
#define マクロ定義名 値
使用例
#define D_KIND_APPLE_ID (0)
定数を示す「マクロ定義名」は、慣例として全て大文字の名前で定義します。
値の後にセミコロンはつけません。うっかり間違えてセミコロンを付けると予期せぬビルドエラーになり、原因がわからなくて困ることがあるので注意しましょう。
define マクロを定義するメリット
あぁた、わたくしの気にしていることをよくもぬけぬけと言い放ったわね!残りわずかな寿命という名のロウソクの火が消える前に、マクロ定義とやらのメリットを語りなさいっ!
御年82のでぇベテランじゃが、まだまだわしは元気じゃ。あだ名をつけるとな、みんながわかりやすくなるじゃろ。皆が喜ぶ顔が見たいんじゃ。
メリット① 名前を付けたことによる可読性の向上
次のプログラムは、先ほどのプログラムをマクロ定義を使って書き換えたものです。
皆さん今度のプログラムは理解できるのではないでしょうか。
マジックナンバーをマクロ定義名に置き換えたことにより、人が見て理解のできるプログラムに変わります。
#include <stdio.h>
#define D_KIND_APPLE_ID (0)
#define D_KIND_BANANA_ID (1)
#define D_KIND_PEACH_ID (2)
void printPrice(int kind)
{
switch (kind)
{
case D_KIND_APPLE_ID:
printf("250円\n");
break;
case D_KIND_BANANA_ID:
printf("100円\n");
break;
case D_KIND_PEACH_ID:
printf("400円\n");
break;
}
}
int main(void)
{
printPrice(D_KIND_PEACH_ID);
return 0;
}
あえてコメントは入れてありませんが、内容は理解できましたよね。このプログラムはマクロ定義を使わないものと全く同じ動作をします。
マクロ定義である「#define」とは、プリプロセッサにより
プログラム中に書かれたマクロ定義名を、値へと置換する「文字列の置換機能」
なのです。
皆さん自身がソースコードの中からマクロ定義名の文字列を見つけて、値へとコピー&ペーストするのと同じことをプリプロセッサは行うのです。
マクロ定義を使うことで、開発者は数値に意味を与えることができ、可読性の高いソースコードを作ることができます。
わしが名付けた「あだ名」は結局は、「値」に戻されてしまうんじゃな~。悲し~の~。
メリット② 定数値の一括変更
マクロ定義は値に名前を付けられるメリット以外にも便利な用途があります。
それは、定数値の一括変更です。
先ほどのプログラムでリンゴのIDの値を「0」から「5」に仕様変更しなければならなくなりました。
この場合、皆さんはリンゴの「0」をプログラムから検索し、「5」に変更しなければなりません。
この時に注意しなければならないのが、「0」という数値はリンゴのID以外にもプログラムの中で使用されることがあり得るということです。
そのため、プログラムを解析し「リンゴの0」を表している数値のみを、漏れなく変更しなければなりません。プログラム規模が大きくなると、この作業は困難になります。
この問題はマクロ定義を行っておくことで簡単に解決します。
マクロ定義の値を編集するだけで、すべてのリンゴの番号は「5」に置換されます。そして、リンゴの番号とは無関係な「0」の値には影響がありません。
このように数値というのは後からメンテナンスしようとするほど困難になっていきます。
最初にプログラムを作りこむときに、しっかりとマクロ定義を行うことで未来の作業が楽になるのです。
名前を付けておくと未来が楽になるんじゃて。未来の自分がするメンテナンス作業が楽になるんじゃ。
良き未来をすごせるように、「マクロ定義」という名の投資をするということじゃな。
define 関数マクロの定義方法と使い方
その年齢でC言語を操るとは、お見逸れいったわ。あなた、じいや2号にしてあげるわっ。じいや2号!さらなるテクニックをお教えなさいっ。
ふぉ、ふぉ、ふぉ、孫娘のようで心躍るのぅ。では、じいやが「関数マクロ」を教えて差し上げよう。
マクロ定義の中には「関数マクロ」と呼ばれる機能が存在します。
関数マクロの書き方
「関数マクロ」は皆さんがこれまで作ってきた関数を、マクロ定義で作るというものです。
とはいっても規模の大きい関数というものではなく、「関数で作るほどでもないが、ちょっとした処理がしたいな」という時に定義します。
定義方法
#define 関数マクロ名(引数) 展開方法
定義例
#define triangle(base, high) ((base) * (high) / 2)
プログラムでは次のように使用することで展開結果に置き換わります。
プログラムの中ではあたかも関数呼び出しかのように書かれていますが、実際は関数ではないのが関数マクロの特徴です。
関数マクロの定義例その①:三角形の面積算出
#include <stdio.h>
// 三角形の面積を求める関数マクロ
#define triangle(base, high) ((base) * (high) / 2.0)
int main(void)
{
double area;
// 面積の算出と表示
// ((10.0) * (30.0) / 2.0) へ置換
area = triangle(10.0, 30.0);
printf("%lf\n", area);
return 0;
}
関数マクロの定義例その②:三角形の面積算出
#include <stdio.h>
// 正方形の面積を求める
#define square(side) ((side) * (side))
int main(void)
{
double area;
// 面積の算出と表示
// ((20.0) * (20.0)) へ置換
area = square(20.0);
printf("%lf\n", area);
return 0;
}
defineとは文字列の置換であると解説しましたが、それは関数マクロでも同じです。
ルールに従い文字を指定の形式に置換するのが関数マクロなのです。
マクロの副作用に注意
マクロ定義は使い方を誤ると、思わぬ結果に置換されるため注意が必要です。
これを「マクロの副作用」と呼びます。
代表的な副作用が発生するケースを紹介します。次の関数マクロを定義したとします。
本来面積は(10.0+5)×(30.0 + 2)÷ 2.0 ⇒ 240.0になるはずですが、計算結果は161.0になってしまいました。
これがマクロの副作用です。副作用を避けたいのであれば少し面倒ですが、各引数要素と全体を()で値を括ってあげることです。
マクロ定義でも関数マクロでも値を()で括ってあげることにより、副作用を発生しないようにできます。
マクロ定義は単純な文字列置換であることを知っておきましょう。
Q&A:define マクロ定義に関するよくある質問
C言語歴47年。でぇベテランのわしがなんでも答えるぞい!
Q:マクロ定義の名前の先頭に「D_」ってついてるのはなぜ?
じいや2号!どうしてじいやが名付ける名前には「D_」が必ず付いているの?じいやのマイルールなの?
そうじゃの~、マイルールでもあるし、そうでないとも言えるの~。
本記事に登場するマクロ定義の名前は「D_KIND_APPLE_ID」のように先頭に「D_」がくっついています。
これは、Defineの「D」を示しており、頭に付けるので「接頭語」と呼びます。
このように名前にルール付けをすることで、皆が同じ認識でプログラムを見られるようになります。「D_~」を見たら「あぁ、これはマクロ定義だな~」といった形で認識するのです。
実際の開発の中でも、名前の付け方にルールを設けることで可読性を向上させる活動をすることは珍しくありません。
Q:定数定義には列挙型もあったが、マクロ定義との切り分けは?
じいや2号!わたくしはすでに「列挙型:enum」による定数の定義方法をを手に入れているわっ。
定数定義はenumとマクロ定義、結局どちらを使うのがよいのかをお教えなさいっ!
良い質問じゃの~。enumの列挙型は使うシーンは限定的じゃな。マクロ定義の方が汎用性が高いんじゃな。
C言語において定数の作り方がいくつかあります。代表的な定数定義がマクロ定義とenumです。
まず、enumについてですが、値が正数値でないといけないという制限があり、浮動小数点は定義できません。
それに対しマクロ定義にはそのような制約がなく、次のように浮動小数点のマクロ定義も作ることができます。
#define PI (3.141592) // 円周率
代表的な浮動小数点のマクロである円周率のPIです。
さらに、マクロ定義とは実は数値である必要性すらありません。次のように文字列を定義することもできます。
#include <stdio.h>
// 文字列をマクロ定義
#define D_HELLO "Hello"
int main(void)
{
printf("%s\n", D_HELLO);
return 0;
}
マクロ定義は「定義名」を単に「値」に置き換える機能です。その仕組みを知った上で利用することです。
結論としてenumとマクロ定義の使い分けの基準ですね。
「enumは正数値で重複しない連番を作りたい時に使う、それ以外の定数はマクロ定義」と覚えておくとよいでしょう。
enumについて知りたい方は『C言語 enum 列挙型【連番の作り方と使いどころを教えます】』の記事も参考にしてください。
課題:define マクロ定義が学べたかを確認しよう
課題1
課題内容
次の定数をマクロ定義を用いて定義せよ。
次の関数を定義せよ。計算の際の円周率はマクロ定義を利用すること。
次のプログラムに上記のマクロ定義と関数を追加し、出力期待結果が表示されることを確認せよ。
#include <stdio.h>
int main(void)
{
double len;
// 円周取得
len = getCircleLength(2.8 + 1.4);
printf("%lf\n", len);
return 0;
}
出力期待結果
26.389373
main.c
#include <stdio.h>
#define D_PI (3.141592)
double getCircleLength(double radius)
{
return 2 * D_PI * radius;
}
int main(void)
{
double len;
// 円周取得
len = getCircleLength(2.8 + 1.4);
printf("%lf\n", len);
return 0;
}
円周率は毎回書くのはしんどいからの~。このような定数はあだ名を付けてやるとええの。名前を付けたらいつでも使えるからの~。
課題2
課題内容
本課題は課題1に追記・変更することで行うものとする。
課題1で作成したgetCircleLength関数と同等の結果を出力する関数マクロを作成せよ。関数マクロ名と引数の半径rは次の形式で定義すること。
CIRCLE_LEN(r)
main関数を次のプログラムに差し替え、出力期待結果が表示されることを確認せよ。
#include <stdio.h>
int main(void)
{
double len;
// 円周取得
len = getCircleLength(2.8 + 1.4);
printf("%lf\n", len);
// 円周取得
len = CIRCLE_LEN(2.8 + 1.4);
printf("%lf\n", len);
return 0;
}
出力期待結果
26.389373
26.389373
main.c
#include <stdio.h>
#define D_PI (3.141592)
#define CIRCLE_LEN(r) (2 * D_PI * (r))
double getCircleLength(double radius)
{
return 2 * D_PI * radius;
}
int main(void)
{
double len;
// 円周取得
len = getCircleLength(2.8 + 1.4);
printf("%lf\n", len);
// 円周取得
len = CIRCLE_LEN(2.8 + 1.4);
printf("%lf\n", len);
return 0;
}
忘れておらんか?関数マクロの引数は括弧で括ることじゃぞ。副作用を起こさんように気を付けるんじゃぞ。