Nodachisoft Nodachi Sword Icon
  
@あまじ✎ 2021年1月16日に更新

第2章30 プリプロセッサ命令のdefine文を使う

イチからゲーム作りで覚えるC言語
第2章29 プリコンパイラ処理とインクルード文@イチからゲーム作りで覚えるC言語 : PREV
NEXT : 第2章31 プリプロセッサ命令でインクルードガードを行う :

この記事でやること

前回はプリプロセッサ(プリコンパイラ処理)と、その代表的な一つである include 文についてお話しました。

今回は、新しくプロジェクト「0250_precompiler_define」を作り、同じく非常によく使われるプリプロセッサ命令の define文について基本的な使い方をお話したいと思います。

define文を使ったソースコードをみてみる

define文を使った例を見てみましょう。

 
cppdefine.c
#include <stdio.h>
 
#define BAG_LIMIT 100
#define PLAYER_NAME "ぷらんく"
#define speak(MESSAGE) printf("%s : %s\n", PLAYER_NAME, MESSAGE)
int main(void) {
  printf("%s : バッグには %d 個まではいるよ!\n"
    , PLAYER_NAME, BAG_LIMIT);
  speak ("まだまだはいるよー!");
  printf("システム:BAG_LIMIT は %d です\n", BAG_LIMIT);
}

このソースコードの中では3つの define 文が使われています。実行結果は下のようになりました。

cppdefine.cの実行結果
ぷらんく : バッグには 100 個まではいるよ!
ぷらんく : まだまだはいるよー!
システム : BAG_LIMIT は 100 です

プレイヤーキャラクタが話しているイメージで、文字列が表示されたと思います。

define文とは

#define がプリプロセッサ命令として読み込まれるときマクロ処理がされます。 マクロ処理とは一定の操作を自動的に行うことで、define文については、ルールに従ってソースコードの文字が置換されるような操作をさせることができます。

このdefine文のマクロ処理には大きく2種類あって、 オブジェクト形式のマクロ(object-like macro)と関数形式のマクロ(function-like macro)があります。

オブジェクト形式のマクロ

オブジェクト形式のマクロは、単純にソースコードに書いてある文字(シンボル{}と呼びます)を別の文字や数字に置換する機能です。 といってもすべてを無条件に置換されてしまうと、むちゃくちゃなソースコードになってしまう恐れもあるので、ルールもあります。 実際に今回のソースコードの中身を見ながらお話したいと思います。

関数形式のマクロ

関数のような引数をもつ define の定義を展開してくれます。 簡単な計算だったら、この関数形式のマクロを使うことで、関数の代わりに、関数とほぼ同じような処理をさせることができます。 ケースバイケースではありますが、関数形式のマクロは、関数を作って動かすのと比べて、高速に動かすことができる、メモリの使用量を削減できるなどのメリットがあります。

マクロを使うメリットって?

なぜこのようなメリットが生まれるかですが、関数を呼び出して計算させるとき、「〇〇関数を呼び出す」処理や「〇〇関数を実行するためのメモリ確保」「〇〇関数終了時の戻る先」などのメモリを確保する処理などがあります。 マクロ関数はそういった処理を省略することができますので、マクロで済ませられるところはマクロで実行するほうが高速だしメモリ使用量も少なくてすむ、と理解してもらえればと思います。 実際には最近のPCやスマホなど、CPUもメモリも高速ですので、そこまでマクロ関数にこだわらなくても良いですし、積極的にマクロ関数を使っていく必要はないかもしれません。

ソースコードの中身を追ってみる

今回のソースコードの中身をみながら、プリコンパイラの処理が走るときの動きを見てみます。

cppdefine.c
#include <stdio.h>

1行目の include文については前回の記事で詳細を書いてましたが、stdio.h の中身をこの1行目に取り込んで、1行目と置き換えます。これにより、printf 関数が使えるようになります。

define文で文字列を数字として定義

cppdefine.c
#define BAG_LIMIT 100

3行目に一つ目の define文が書いてあります。これはオブジェクト形式のマクロの書き方で、置換するマクロとなります。define文も include文と同じように、改行が入るまでが一つのプリプロセッサ命令です。

下のように書くことで、ソースコード内のBAGLIMIT という文字列は 100 に変換されるよう定義しており、この行以降をプリコンパイルするとき BAGLIMIT が数値の 100 に置き換わります。

define文の書き方
#define   シンボル   置き換えた後

define、シンボル、シンボルを置き換えた後 のそれぞれの間には半角スペースを入れる必要があります。

define文で文字列をchar型配列の文字列として定義

cppdefine.c
#define PLAYER_NAME "ぷらんく"

4行目ではソースコードの文字列を、char型配列の “ぷらんく” に変換するdefine文が書いてあります。これもオブジェクト形式のマクロです。

この行以降、ソースコード内の PLAYER_NAME という文字列を “ぷらんく” に置き換わります。

define文で関数マクロを定義
#define PLAYER_NAME "ぷらんく"
#define speak(MESSAGE) printf("%s : %s\n", PLAYER_NAME, MESSAGE)

5行目では関数形式のマクロを定義しています。下のような書き方で定義ができます。

define書き方
#define   置き換える関数に似た文字列   置き換えた後

今回も「speak(MESSAGE)」という関数のような形を定義しています。 define文はスペースで置換前と後を区切っています。

  • 置換前は「speak(MESSAGE)」
  • 置換後は「printf(“%s : %s\n”, PLAYER_NAME, MESSAGE)」

となっています。ここで、置換後の PLAYER_NAME は、4行目で “ぷらんく” というchar型配列の文字列に変換されていますね。ですので

  • 置換前は「speak(MESSAGE)」
  • 置換後は「printf(“%s : %s\n”, “ぷらんく”, MESSAGE))」

に変換されています。また、関数形式のマクロの特徴で引数の部分も合わせて置換してくれるという特徴があります。例えば、今回のspeakではMESSAGEという引数を持っています。

たとえば、この define の行以降に、ソースコードに「speak(“あいうえお”)」というspeak 関数を呼び出しているような記載があった場合、今回の置換対象になります。置換後は下の形になります。

  • 置換前は「speak(“あいうえお”)」
  • 置換後は「printf(“%s : %s\n”, “ぷらんく”, “あいうえお”)」

なお、define文で定義した speak(MESSAGE)の置換後の末尾に「;」セミコロンが書いていないので、定義した speakマクロ関数を使うときは、ちゃんと末尾に命令の終わりである「;」を付けてあげる必要があります。

複数の引数

なお、関数マクロの引数は1つだけでなく、複数も定義できます。例として キャラクタの名前としゃべるしゃべるる関数マクロ、maxを見てみましょう。

 
cppdefine.c
#define talk(a,b) printf("%s : %s",a,b)

これでソースコードのdefine の後で talk(“村人”,”こんにちは!”); などと書けば、

  • 置換前は「talk(“村人”,”こんにちは!”);」
  • 置換後は「printf(“%s : %s”,”村人”,”こんにちは!”);」

のようにプリコンパイル処理で置換されます。

ソースコードのプリコンパイル処理後

ここまでの define 定義を受けて、ソースコードはどのように置換処理されるかを確認してみましょう。こちらはプリコンパイル処理前です。

 
cppdefine.cのプリコンパイル前
#include <stdio.h>
 
#define BAG_LIMIT 100
#define PLAYER_NAME "ぷらんく"
#define speak(MESSAGE) printf("%s : %s\n", PLAYER_NAME, MESSAGE)
int main(void) {
  printf("%s : バッグには %d 個まではいるよ!\n"
    , PLAYER_NAME, BAG_LIMIT);
  speak ("まだまだはいるよー!");
  printf("システム:BAG_LIMIT は %d です\n", BAG_LIMIT);
}

プリコンパイル処理後はこちらです。

 
cppdefine.cのプリコンパイル後
~stdio.h の中身 が include文で挿入(省略) ~
 
int main(void) {
  printf("%s : バッグには %d 個まではいるよ!\n"
    , "ぷらんく", 100);
  printf("%s : %s\n", "ぷらんく", "まだまだはいるよー!");
  printf("システム:BAG_LIMIT は %d です\n", 100);
}

include文やdefine文がプリコンパイラで処理され、ソースコードが置換されています。 なお、もともとのソースコードの10行目の printf の中身に、defineで置換対象のシンボル BAG_LIMIT が ” ~ ” の中に書いてありますが、ソースコードの文字列(”~”)の中は define の置換の対象外となります。

 
cppdefine.cのプリコンパイル前
printf("システム:BAG_LIMIT は %d です\n", BAG_LIMIT);

上で置換されるのは、第2引数の BAGLIMIT のみです。第1引数の文字列の中の BAGLIMIT は置換されません。

関数マクロを利用するときの書き方

関数マクロを定義するときは、「speak(MESSAGE)」のように半角スペースを入れてはいけないとお話しました。この関数マクロをソースコードの中で呼び出しするときは、このような制限はありません。

speak  (  MESSAGE  )

のように、通常の関数を利用するときのようにみやすく、半角スペースを入れてあげても問題なく関数マクロを利用できます。

ソースコードの中身はこれで一通り確認おしまいです。

よくある間違った書き方

define で関数マクロを定義するときに、よくある書き間違えを書いておきます。

スペースを誤った箇所に書く

たとえば今回の変換で「speak(MESSAGE)」という関数マクロを定義しましたが、このようなとき変換する関数マクロの文字列のどこかにスペースが入っていないことに注意してください。

例えば、ここで「speak□(MESSAGE)」(※□は半角スペース)などのように書いてしまうと、speak という文字列を「(MESSAGE) printf(“%s : … 略 … MESSAGE)」に置換するように認識してしまうため、注意が必要です。

  • 定義は「#define speak□(MESSAGE)□printf(“%s : %s\n”, PLAYER_NAME, MESSAGE)」※□=半角スペース
  • 置換前は「speak(“てすと”);」
  • 置換後は「(MESSAGE)(“てすと”) printf(“%s : %s”,”村人”,(MESSAGE));」

そしてこのような書き間違えは、コンパイルしたときに置換が発生する箇所での文法エラーとして検知されたりして、直しづらいバグとなることがあります。

define 定義時の末尾に「;」セミコロンを書く

文法的に誤りではありませんが、define文で定義するとき末尾に「;」を書くと、「;」も一緒に変換されます。例を見てみましょう。

 
誤ったdefine例(プリコンパイル前)
#include <stdio.h>
#define  plusOne(a)   (a)+1;
int main(void) {
  printf("100+1 は %d です。", plusOne(100));
}

この時、実際に上のようなコードを実行しようとすると、コンパイルできずにエラーとなると思います。 2行目の define で行末尾に「;」が入っていることが原因です。 上のコードのプリコンパイル処理が終わると、下のようなソースコードに変換されます。

 
誤ったdefine例(プリコンパイル後)
~stdio.h の中身~
int main(void) {
  printf("100+1 は %d です。", (100)+1;);
}

plusOne(100)は「(100)+1;」に置換されており、変な箇所で「;」が入ったことで、文法エラーとなってしまっています。 このようなケースのエラーも、なかなかわかりづらい上に、ついやってしまいがちですので、気を付ける必要があります。

役に立つ関数マクロや定義

関数マクロで非常に役立つものをいくつかピックアップしておきたいです。

メタ情報を表示するマクロ

C言語に最初から用意されている、プログラムをデバッグ(エラーを取り除く処理)に便利な関数マクロがあります。 実際にどこかの行でエラーが発生したとき、そのエラーを突き止めるために、”どのソースコードファイルの〇〇関数のn行目”などの情報があると確認がはかどりますね。 そういったソースコードの時の情報(メタ情報)をコンパイルして機械語に変換したときにも埋め込むことができます。

 
メタ情報を表示するマクロを使った例
#include <stdio.h>
 
int main(void) {
  printf("コンパイルされた時間は %s の %s です。\n"
    , __DATE__, __TIME__);
  printf("現在、実行している関数は %s の %s の %d 行目です。\n"
    , __FILE__, __func__, __LINE__ );
}

上を実行すると、下のような結果が表示されます。

 
メタ情報を表示するマクロを使った例
コンパイルされた時間は Jan 15 202106:16:25 です。
現在、実行している関数は c:\~中略~\cppdefine.c の main の 7 行目です。
  • __DATE__ はプリコンパイル処理が実行されたときの日付が入ります。形式は”Mmm dd yyyy”です。月の表記は asctime 関数という日時を文字で表示するための関数と同じ内容で表示されます。(例では1月「Jan」と表示)
  • __TIME__ はプリコンパイル処理が実行されたときの時刻が入ります。形式は”hh:mm:ss”です。こちらも__DATE__と同様、asctime 関数の結果と同じように表示されます。
  • __FILE__ は、記載しているソースコードファイルのディスク上の場所(パス)を文字列で変換します。
  • __LINE__ は、記載しているソースコードの行を long 型に変換してくれます。
  • __func__ は厳密にいうとマクロではなく、プログラム実行時にウラで自動的に生成される文字列変数なのですが、マクロと似たような感覚で使うことができます。現在実行している関数の名前が文字列変数で入っています。例のプログラム内では main 関数の実行中であることがわかります。

あとがき

今回もinclude文に続き、プリプロセッサ命令のdefine文についてお話しました。stdio.h など、〇〇.h といった標準で用意されているヘッダーファイルの中身でも沢山の便利な関数マクロが定義されていたりしますので、自分で積極的に自作関数マクロを作らなかったとしても、読んで使えるようになるとプログラミングがはかどると思います!

非常に参考になったサイトさまや、参考文献など

ページの更新履歴

更新日 更新内容
更新なし
イチからゲーム作りで覚えるC言語
第2章29 プリコンパイラ処理とインクルード文@イチからゲーム作りで覚えるC言語 : PREV
NEXT : 第2章31 プリプロセッサ命令でインクルードガードを行う :
 
 
送信しました!

コメント、ありがとうございます。

なんかエラーでした

ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。

Twitter:@NodachiSoft_jp
お名前:
 
連絡先:
 
メッセージ:
 
戻る
内容の確認!

以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください

お名前:
 
連絡先:
 
メッセージ:
 
Roboto からの操作ではないという確認のため確認キーを入れてください。
確認キー=95
戻る
 / 
送信確認へ
コメント欄
コメント送信確認へ

関連ありそうな記事(5件)です!

ゲーム等で使えるつなぎ目のないループするテクスチャ画像の作り方

#ツール#ゲームプログラミング✎ 2021-01-24
ゲームなどで使えるループ画像、パターンテクスチャのツール、手動での作り方をまとめ
広告領域
追従 広告領域
目次
第2章30 プリプロセッサ命令のdefine文を使う
第2章30 プリプロセッサ命令のdefine文を使う
この記事でやること
この記事でやること
define文を使ったソースコードをみてみる
define文を使ったソースコードをみてみる
define文とは
define文とは
オブジェクト形式のマクロ
オブジェクト形式のマクロ
関数形式のマクロ
関数形式のマクロ
ソースコードの中身を追ってみる
ソースコードの中身を追ってみる
define文で文字列を数字として定義
define文で文字列を数字として定義
define文で文字列をchar型配列の文字列として定義
define文で文字列をchar型配列の文字列として定義
複数の引数
複数の引数
ソースコードのプリコンパイル処理後
ソースコードのプリコンパイル処理後
関数マクロを利用するときの書き方
関数マクロを利用するときの書き方
よくある間違った書き方
よくある間違った書き方
スペースを誤った箇所に書く
スペースを誤った箇所に書く
define 定義時の末尾に「;」セミコロンを書く
define 定義時の末尾に「;」セミコロンを書く
役に立つ関数マクロや定義
役に立つ関数マクロや定義
メタ情報を表示するマクロ
メタ情報を表示するマクロ
あとがき
あとがき
非常に参考になったサイトさまや、参考文献など
非常に参考になったサイトさまや、参考文献など
ページの更新履歴
ページの更新履歴
Nodachisoft © 2020