苦しんで覚えるC言語 18章 マクロ機能 P372 – P386

不変の値の取り扱い

プログラムの中で処理に使用する値が全く変化しないものが出てきます。
例えば、1年間の月数は12ヵ月です。これは変化するものではありません。

この様な変化しない値を変数で設定してしまうと、間違って処理の過程で代入されて、書き換わることがあるとバグになってしまいますので、以下の様に不変の値として定義することができます。

 #define 名前 数値

先ほど例に出した1年間の月数であれば、こんな感じでしょうか。

 #define MONTH_NUM 12

#define というのは定数を定義する際に冒頭につけることが決まっている記述です。
MONTH_NUMは、名前になり私がそれらし名前を勝手につけました。
12というのは、1年間が12ヵ月なので12としています。

イメージとしては、MONTH_NUM = 12 と定義されて書き変わることがない。という感じでしょうか。
定数の定義名は大文字にすることが一般的です。
小文字でもエラーにはなりませんが、内部関数で宣言している変数との見分けがつかないこともあり、定数は全て大文字にする方が、分かりやすいと思います。

もし値を変更したい場合、数値の記述を変更してコンパイルしなおし。になります。
組込の場合、機器への書き込みをし直す必要があるので、日本国内に出荷されている場合は、現地に行って該当機器のプログラムを書き換えすることになりますので、致命的な変更となった場合は、後々のフォローが大変になります。

プログラム上ではどこに記述するのか、参考書を例に見てみましょう。

#include <stdio.h>
#define EXCISETAX 0.03   /* ここで定数を宣言 */

int main(void)
{
    int price;
    printf("本体価格:");
    scanf("%d",&price);
    price = (int)((1 + EXCISETAX) * price);  /* 定数使用 */
    printf("税込価格:%d\n",price);

    return 0;
}

定数の宣言は、ファイルの冒頭で行います。
#includeも冒頭でしたので、同じようにソースファイルの冒頭で書くようにします。

以降は、ファイル内であればどこで使用することも可能です。
上記の例では、
 price = (int)((1 + EXCISETAX) * price); /* 定数使用 */
と書かれた計算式の一部に、EXCISETAXが使用されています。

代入された数値に変換して解説してみましょう。
例えば、priceはscanfで100が入力されていたとします。

(int)((1 + 0.03) * 100); /* 定数使用 */

この計算をして、priceに上書き保存されるイメージになります。
つまり、103という値が最終的にpriceに格納されます。

()がたくさん付いていますが、(1+0.03)が最初に計算されます。
続いて、(int)(1.03 * 100)が計算されます。
(int)は過去の解説でも出てきましたキャストになりますので、計算結果をint型として扱うことを指示するもので、priceという変数がint型なので、それに合わせています。

どうでしょうか?
0.03と数値で直接書いても構わないのですが、ソフト設計の思想として数値の直接入力は、NGとしている企業が大半です。
理由はいくつかあるのですが、数値の意味が分からないというのが主たるものです。

税率として扱うのであれば、ZEIRITUとかTAXなど、連想しやすい名前を付けて0.03を格納しておくと、可読性があがります。

ちなみに、0.03というのは3%の税率を意図しているのだと思いますが、税率はいつ変更されるかわかりません。
上記の途中で記載しましたが、3年後に税率が変更された場合、プログラムの0.03の数値を変更して、再度コンパイルを行い、出荷した機器に反映する必要があります。

出荷済みの機器に影響を与えない変更であれば、そのままで良いですが致命的な変更になる場合は、フォローが大変になるので、恒久的に不変な値を定義するか、機器として変わっても変更対応が取れるものに限って定義をするようにしてください。

文字列に名前を付ける

文字列データも#defineで定義をすることが可能です。
組込機器に文字列を使う機会が少ないのですが、一様解説をしたいと思います。

#include <stdio.h>
#define AUTHOR "MMGames"

int main(void)
{
    printf("作者名:%s\n", AUTHOR);

    return 0;
}

参考書のコード例になりますが、数値と全く同じ要領です。
#define から始まり、定義名を書いた後、文字列なので ” “ で囲みます。
文字は ” “か ‘ ‘で囲って扱うことを当初に解説をしています。

main関数内のprintf関数で文字表示を指示して、AUTHORに格納されたMMGamesが表示されます。

その他の方法による定数宣言

#define 定義で定数を宣言する方法は王道になり、一般的にも良く使われます。
他の方法として、const定数を使用して宣言する方法があります。

#include <stdio.h>

int main(void)
{
    const double EXCISETAX = 0.05;
    int price;
    printf("本体価格:");
    scanf("%d",&price);
    price = (int)((1 + EXCISETAX) * price);
    printf("税込価格:%d\n",price);

    return 0;
}

main関数の先頭で、const double EXCISETAX = 0.05; と書かれている箇所があります。
ここが、#define定義と同等の内容になります。

double EXCISETAX = 0.05; だけを見ると、これまで解説した変数の宣言と同じです。
型 + 変数名 を用いて、宣言と同時に0.05を初期値代入している。というものです。

この宣言の先頭に const を付記するだけで、変更不可の媒体となります。
但し、有効範囲は異なります。

#define はソースファイルの冒頭で記述をし、そのファイル全体に対して有効となります。
constは、関数の先頭で記述をし、その関数内に対して有効となります。

全体的に定数化する必要はないけれど、何度も処理がcallされる関数内で変化して欲しくない数値は、const定義することが可能です。

enum定数

最後に定数の宣言の方法がもう一つあります。
それは、enum を使って宣言する方法です。

enumはこれまで解説した定数と同じなのですが、
#defineやconstはひとつずつ宣言することになりますが、enumは複数の宣言を一度にすることが可能です。
宣言の書き方は参考書に習って以下となります。

enum{
名前,
名前,
名前
}

ここでは3つの要素を宣言していますが、いくつでも宣言することが可能です。
ポイントは、名前の後は , (カンマ)で区切ること、最後の要素の名前の後はカンマは不要であること。です。

参考書のコード例を見てみましょう。
#define で書かれた例は見てそのままになりますので割愛します。

enum{
    STATE_NORMAL,  /* 通常 */
    STATE_POISON,  /* 毒 */
    STATE_NUMBLY,  /* マヒ */
    STATE_CURSE    /* 呪われ */
};

ここで気づくことは、数値の代入が無いと言うことです。
enumは、宣言時に数値を記載しない場合、自動的に数値が割り振られて代入されます。

割り振りされる数値は、上から順に0,1,2,3となります。
つまり、0から要素数分だけ割り当てされると言うことです。

設計者が意図的に数値設定したい場合は、以下の様に書けば設定が可能です。

enum{
    STATE_NORMAL = 100,  /* 通常 */
    STATE_POISON = 101,  /* 毒 */
    STATE_NUMBLY = 200,  /* マヒ */
    STATE_CURSE = 300    /* 呪われ */
};

あるいは、上記は全ての要素に対して数値を設定しましたが、以下の様に書くと設定した数値以降は1ずつ加算されて設定されます。

enum{
    STATE_NORMAL,  /* 通常 */
    STATE_POISON,  /* 毒 */
    STATE_NUMBLY = 100,  /* マヒ */
    STATE_CURSE    /* 呪われ */
};

STATE_NORMALは 0
STATE_POISONは 1
STATE_NUMBLYは 100
STATE_CURSEは 101

間違いが少ない様に、要素には全て代入値を書くようにするか、全く書かないようにするか、
どちらかが良いと思います。
要素の途中で値を設定することで、値の系統が変わることに気づかないミスが出るのは避けた方が賢明と思います。

簡易的な関数の表現

#defienは定数を宣言するのに使用することは解説した通りですが、もう少し高度に宣言をして使うことが可能です。

それは、簡易的な演算処理を定数と同じ様に定義することができる。というものです。
参考書の例を元に見てみましょう。

#include <stdio.h>

#define PRINT_TEMP printf("temp = %d\n", temp)

int main(void)
{
  int temp = 100;
  PRINT_TEMP;

  return 0;
}

#define PRINT_TEMP と書かれていますが、ここまではこれまで解説した内容と同じです。
以降の、printf(“temp = %d\n”, temp)がこれまでと異なる点になります。

これまでは、値や文字列が設定されていましたが、処理を設定することも可能です。
main関数内で PRINT_TEMP が書かれていますが、ここは、printf(“temp = %d\n”, temp)に置き換わって処理されます。

つまり、以下の書き方をした時と同じということです。

int main(void)
{
  int temp = 100;
  printf("temp = %d\n", temp);

  return 0;
}


#define の機能は、単なる置き換えを示唆するもので、数値や文字列に限定されません。
その為、処理となるものも置き換え指定が可能ということです。

マクロという簡易関数

処理に置き換えすることが可能ということで、もう少し複雑な事例を解説したいと思います。
自作関数を作ることで対応できるが、自作関数を作っても1行で終わってしまう様な処理の場合、#define で置き換え処理を書くことが可能です。

例えば、参考書を例に以下を見てみましょう。

#include <stdio.h>

#define PRINTM(X) printf("%d\n",X)

int main(void)
{
    int a1 = 100;
    int a2 = 50;

    PRINTM(a1);
    PRINTM(a2);

    return 0;
}

PRINTMという定義名で、引数を持たせる様にして定義させています。
自作関数で書けば、以下の様に記述できます。

 void PRINTM(int X)
{
printf(“%d\n”,X)
}

これでも問題はありません。
ただ、1行だけの処理になり、且つ関数のプリプロセッサ宣言を書かなければならないので、
#defineで1行に集約することで簡易関数とする方が便利である。とも言えます。
特に、何度も使われる処理であれば、効果は大きいので、一般的にも良く用いられます。

詳細な解説にもなりませんが、
PRINTM(a1);
PRINTM(a2);
は printf(“%d\n”,X)に置き換えされ、且つa1、a2がXとして渡されます。

処理のイメージは関数と同じなので、簡易関数として機能していると言われる所以になります。
一般的には、マクロとかマクロ処理マクロ定義と呼ばれることが多いです。

関数とは大きく異なる点があるとすれば、置き換えされると言うことです。
関数はcallされるので、用意された自作関数が呼び出されて処理がされます。
他方、#defineの方は、呼び出されるのではなく、処理が置き換えされます。

関数は呼び出されるのに時間を有しますが、マクロは置き換えされるので呼び出しされる時間はありません。
多投されるのであれば、塵も積もればとなり、処理時間の短縮に起因します。

しかし、#defineにもデメリットがあり、置き換わるということは、
大きな処理を書いてしまうと、全ての箇所が大きな処理として置き換わってしまい、プログラムメモリの容量を圧迫することになります。

その為、マクロを定義する場合は、極簡易的な演算処理に留めることをお勧めします。

マクロの副作用

デメリットはご紹介の通りですが、使い方として演算の優先度と置き換え内容が掌握されていないと、以下の様な問題に遭遇します。
参考書の例を見ながら解説したいと思います。

#include <stdio.h>

#define GET_TRAPEZOID_AREA (A,B,H)  (A + B) * H / 2

int main(void)
{
    int up,down,h,s
    pritnf("上底、下底、高さ:");
    scanf("%d,%d,%d",&up,&down,&h);
    s = GET_TRAPEZOID_AREA(up,down,h + 3);
    printf("面積:%d\n",s);

    return 0;
}

実行結果としては、s = GET_TRAPEZOID_AREA(up,down,h + 3); でs=76となります。
h + 3しているのに、結果が期待する値と異なります。

GET_TRAPEZOID_AREAのマクロが置き換えする通りに当て嵌めると、
(up + down) * H + 3 / 2 となってしまい、演算の優先順が機能して異なる結果になったというわけです。

この解決方法は2通りあり、
①マクロを使用する側で優先順を明確にする。
 GET_TRAPEZOID_AREA(up,down,(h + 3));
( ) 括弧を付けて優先演算を明記させる

②マクロ側で優先順を明確にする。
 GET_TRAPEZOID_AREA (A,B,H) ((A) + (B)) * (H) / 2
( )括弧を付けて、置き換えされた値や式が優先処理になるように明記する。

①と②はどちらも一般的に見受けられるので、分かりやすい方で統一されることが望ましいと思います。