定数とマクロ

const定数

定数とは、一度値を決めたら変更できない入れ物のことです。
変数は値を「変」えることができる入れ物ですが、定数は値が「定」まった入れ物、という意味です。

定数を宣言するにはデータ型の指定の前にconstという修飾子を使用します。


#include <stdio.h>

const double pai = 3.14159265;

int main()
{
    int hankei = 5;
    printf("半径が%dの円の面積は%fです。",
        hankei, hankei * hankei * pai);
    getchar();
}
半径が5の円の面積は78.539816です。

3行目が定数の宣言です。
定数は変数とは違い、その値を後から書き換える(代入する)ことはできません。
(代入をしようとするとコンパイルエラーになります)
グローバル変数のような「どこからでも書き換えられる」という問題はないので、プログラムのどこからでも参照できるようにしても大きな問題はありません。

サンプルコードのように、後から変更する予定がなく、複数の箇所で使用される値があるならば定数にしてしまった方が便利です。
仮に後からコードの修正があった場合でも、一か所を修正するだけでコード全体に変更が適用できます。


#include <stdio.h>

const char tabemono[] = "ハンバーグ";

int main()
{
    printf("私の好物は%sです。\n", tabemono);
    printf("しかしA君は%sが苦手です。\n", tabemono);
    getchar();
}

このようなコードでは、3行目の「ハンバーグ」を別の食べ物に書き換えるだけで7行目と8行目の表示内容を変更することができます。

上掲コードではグローバル変数にconstを使用していますが、ローカル変数にもconstを使用することができます。


#include <stdio.h>

int main()
{
    const int num = 0;
}

定数を示す「印」

定数は便利ですが、グローバル定数にするとやはりそれなりに欠点があります。
定数は値が書き換えられる心配はありませんが、よく使われる名前を定数名に使用すると、今使用している値がローカルな値なのかグローバルな値なのか混同してしまうことがあります。
そのため、定数には定数とわかるような「印」を定数名に付けるなどの工夫をすると良いでしょう。
グローバル値をやたらと使い過ぎない、というのも大事です。


//頭文字にc_を付ける
const int c_teisuu = 1;

//頭文字にkを付ける
const int kTeisuu = 1;

//全て大文字にする
const int TEISUU = 1;

どのような方法で区別するかは人それぞれですが、できるだけ統一した方法にすべきです。

constの位置

constはデータ型の前後のどちらかに書くことができます。
意味は変わりません。


const int num1 = 0;
int const num2 = 0;

ただしポインタ変数にconstを使用する場合はconstの位置によって意味が変わります。
詳しくはポインタ変数とconstで説明します。

引数に定数を使用する

const修飾子は関数の仮引数に指定することもできます。


#include <stdio.h>

void Func(const int arr[], const int length)
{
    for (int i = 0; i < length; i++)
    {
        printf("%d\n", arr[i]);
    }
}

int main()
{
    int numbers[] = { 1, 2, 3, 4, 5 };
    Func(numbers, sizeof(numbers) / sizeof(numbers[0]));
    getchar();
}
1
2
3
4
5

constが指定された引数は書き換えが禁止されます。
書き換えようとするとコンパイルエラーとなります。


void Func(const int arr[], const int length)
{
    //どちらもコンパイルエラー
    length = 10;
    arr[0] = 10;
}

特に配列は、要素を書き換えると実引数(関数の呼び出し元)の配列の要素も書き換わってしまいます。
書き換わって困る場合はconstを指定しておきましょう。
(ただし無理やり書き換える方法も存在します)

int型などの場合は関数内で書き換えても呼び出し元に影響しませんが、書き換える必要がない値は積極的にconstを指定しておくとミスが減らせます。

マクロ

定数にはもうひとつ、マクロという方法もあります。


#include <stdio.h>

#define PAI 3.14159265

int main()
{
    int hankei = 5;
    printf("半径が%dの円の面積は%fです。",
        hankei, hankei * hankei * PAI);
    getchar();
}

マクロを使用するには#defineというキーワードを使用します。
先頭に#記号がつけられる処理はプリプロセッサといい、コンパイルを開始する前に行われる処理(プリプロセス)を表します。

#defineキーワードに続いて、半角スペースを開けてマクロ名を書きます。
その後に半角スペースを空け、値を直接記述します。

マクロは変数やconst定数のような「データの入れ物」ではなく、データ型を持ちません。
変数のように値を代入して使うものではなく、コードのコンパイルの直前に、コードの記述そのものを置き換えるのがマクロの機能です。

つまり、上のコードはコンパイルの直前に以下のコードに置き換えられた上でコンパイルされます。


#include <stdio.h>

int main()
{
    int hankei = 5;
    printf("半径が%dの円の面積は%fです。",
        hankei, hankei * hankei * 3.14159265);
    getchar();
}

新しい型を作る

マクロはコードを直接置き換える機能です。
つまり、以下のようにすると既存の型に別名を付けるのと同じことになります。


#include <stdio.h>

#define newint int

int main()
{
    newint num = 10;
    printf("%d", num);
    getchar();
}

このコードは「newint」という新しいデータ型を作っています。
コンパイル時には「newint」は「int」に置き換えられるため、newintはint型の別名ということになります。

このような「既存のデータ型に別名を与える」使い方は、C言語のライブラリやWindowsの機能(API)を使う場合に頻繁に登場します。
(ただし、#defineで実装されているとは限りません)

マクロ関数

マクロをただのグローバル定数として使用するだけであれば、const定数と大きな違いはありません。
const定数との決定的な違いはマクロ関数です。


#include <stdio.h>

#define MAX(a, b) (a > b ? a : b)

int main()
{
    int num1 = 5;
    int num2 = 8;

    printf("大きい方の値: %d",
        MAX(num1, num2));
    getchar();
}

このように書くと、マクロを関数のように使うことができます。
上のコードは以下のコードと同じ意味となります。


#include <stdio.h>

int main()
{
    int num1 = 5;
    int num2 = 8;

    printf("大きい方の値: %d",
        (num1 > num2 ? num1 : num2));
    getchar();
}

マクロ関数MAXは、普通に関数で定義した場合とほぼ同じ感覚で使用できます。

ただしこのマクロ関数MAXは問題を引き起こす可能性があります。
(後述するマクロの注意点を参照)

空白の取り扱いに注意

マクロ関数の関数名と引数の間には空白を開けてはなりません。


//ダメな例
#define MAX (a, b) (a > b ? a : b)

これはMAX(a, b) (a > b ? a : b)というコードに置き換える処理(ただのマクロ)になります。


#include <stdio.h>

int main()
{
    int num1 = 5;
    int num2 = 8;

    printf("大きい方の値: %d",
        (a > b ? a : b)(num1, num2));
    getchar();
}

もちろんこのコードはコンパイルできません。

マクロ関数のメリット

先ほど作成したマクロ関数MAXのような値の大小を比較する関数を作る場合、引数のデータ型を指定する必要があります。
int型同士の比較ならば引数をint型にした関数を、double型同士の比較なら引数をdouble型にした関数がそれぞれ必要となります。
関数の中の処理自体は同じなのに、たくさんのコードを書かなければなりません。

マクロはコードを置き換える機能で、データ型に依存しません。
上のマクロ関数MAXは、引数がint型でもdouble型でも、比較可能な値であればそのまま利用できます。
データ型に依存しないで関数(のような機能)を使えるのは利点と言えます。
(C++などの新しい言語では、データ型に依存しない関数を作れるものもあります)

その他、コンパイルの前にコードが置き換えられるので、関数の呼び出しにかかるオーバーヘッド(余分なコスト)がない、という利点もあります。
ただ最近のパソコンは高性能ですからこれはメリットというほどのものではないかもしれません。

マクロ関数のデメリット

マクロ関数はコードの置き換え機能で、変数のような「値の使いまわし」ではありません。
結果として実際のコードへの展開後のコード量が増えることになり、実行ファイルのサイズが大きくなります。
これはマクロ関数の処理が複雑になるほど展開後のサイズが大きくなります。

また、後述する注意点があり、安易に使用するとバグの原因となります。

マクロ関数の注意点

マクロ関数は通常の関数よりも注意して使用する必要があります。
ただのコードの置き換え機能なのですが、それゆえに予想外のコードに展開されてしまうことがあるからです。


#include <stdio.h>

#define KAKEZAN(a, b) (a * b)

int main()
{
    printf("%d",
        KAKEZAN(1 + 2, 3 + 4));
    getchar();
}
11

マクロ関数KAKEZANは、受け取った二つの引数を単純に掛け算しているだけです。
そして、「3 × 7」の結果を求めるつもりでマクロ関数を使用しています。
「21」と表示されることを期待していますが、結果は「11」となっています。

その原因は、マクロを展開すると以下のようになるからです。


#include <stdio.h>

int main()
{
    printf("%d",
        (1 + 2 * 3 + 4));
    getchar();
}

マクロ展開の結果、計算の優先順位が変わってしまうので意図した値とならなかったのです。

これを正しく書き直すと以下のようになります。


#include <stdio.h>

#define KAKEZAN(a, b) ((a) * (b))

int main()
{
    printf("%d",
        KAKEZAN(1 + 2, 3 + 4));
    getchar();
}
21

このコードは正しく「21」と表示されます。
これは以下のコードと等価になります。


#include <stdio.h>

int main()
{
    printf("%d",
        ((1 + 2) * (3 + 4)));
    getchar();
}

マクロを利用する場合は、展開時にどのような形になるのかをしっかりと確認しましょう。
できだけ括弧を付けて計算結果が変わらないようにするなど、細心の注意が必要です。

マクロの副作用

上記のようなケースはマクロを注意深く書くことでバグを避けることができますが、以下のケースではマクロを使用すべきではありません。


#include <stdio.h>

#define SQUARE(x) ((x) * (x))

int main()
{
	int num = 2;	

	//2の2乗のつもり
	printf("%d\n",
		SQUARE(num++));

	//3の2乗のつもり
	printf("%d\n",
		SQUARE(num++));

	getchar();
}
4
16

2の2乗を求めた後、インクリメントをして3の2乗を求めるつもりで上記のようなコードを書いたとします。
しかし実際には二回目の呼び出しは4の2乗になってしまっています。

上記のマクロ関数は以下のコードと同等です。


((num++) * (num++))

マクロの展開により変数numのインクリメントが二回行われることになってしまい、意図した値とはならないのです。

上記のコードはC言語の仕様上「未定義」の動作となっています。
上記の実行結果はVisualStudioで実行した場合です。

同じコードをgccというコンパイラでコンパイル&実行すると「6」「20」という結果となります。
これは変数numのインクリメントの実行タイミングが異なるためです。


//VisualStudioの解釈
((2++) * (2++))
((4++) * (4++))
//両方から値を取り出してから2回インクリメント

//gccの解釈
((2++) * (3++))
((4++) * (5++))
//値を取り出す度にインクリメント

ひとつの式で同じ変数を二回以上変更する処理を行う場合、動作は未定義となります。
C言語の仕様上の未定義の動作というのは、各コンパイラによって動作が異なるということを意味します。
極端なことを言えばプログラムを停止させるコンパイラがあっても、それは仕様上構わないのです。
上のコードもどちらの動作が正しいかはC言語の仕様上決められていないので、移植性がなく、こういったコードは避けるべきです。
(やや難しいですが、詳しくはEXP30-C. 副作用が発生する式の評価順序に依存しないで述べられています)