関数形式マクロ 解答ページ | Programming Place Plus C言語編 第28章

トップページC言語編第28章

問題①

問題① 実引数で指定された値の絶対値を返す関数形式マクロを作ってください。


#include <stdio.h>

#define ABS(val)        (val) < 0 ? -(val) : (val)

int main(void)
{
    int vali = -15;
    double vald = -3.57;

    printf("%d\n", ABS(vali));
    printf("%d\n", ABS(vali - 30));
    printf("%lf\n", ABS(vald));
}

実行結果:

15
45
3.570000

標準関数にも絶対値を求める abs関数があります。関数の方がインクリメントにまつわるマクロの問題を回避できるので安全ですが、型が限定される欠点があります。

実際、abs関数は int型専用なので、long int型用に labs関数、float型用に fabsf関数、double型用に fabs関数、long double型用に fabsl関数がそれぞれ別個で用意されており、使い分けなければなりません。

関数形式マクロとして実装するにあたっては、ABSマクロの実引数を (vali - 30) のような計算式にしても正しい結果が得られるかどうかは1つのポイントでしょう。ABSマクロの置換後の文字の並びのように、仮引数val は1つ1つ ( ) で囲まないと、式を与えたときに正しい結果が得られない可能性があります。もし、次のような ( ) で囲まずに定義すると、

#define ABS(val)        val < 0 ? -val : val

ABS(coli - 30) の置換結果は、以下のようになります。

coli - 30 < 0 ? -coli - 30 : coli - 30

条件演算子の結果が真の場合、coli の値にマイナスの符号を付け、それを -30 した値が返されることになってしまいます。

問題②

問題② 配列の名前を実引数に与えると、その配列の要素数を返すような関数形式マクロを作成してください。


#include <stdio.h>

#define SIZE_OF_ARRAY(array)    (sizeof(array) / sizeof(array[0]))

int main(void)
{
    int i_array[] = {0, 1, 2, 3, 4};
    double d_array[] = {0.0, 1.0, 2.0, 3.0, 4.0};

    printf("%zu\n", SIZE_OF_ARRAY(i_array));
    printf("%zu\n", SIZE_OF_ARRAY(d_array));
}

実行結果:

5
5

このマクロは非常に有用なもので、よく使われています。

配列の要素数を計算する方法は、第25章で取り上げました。「sizeof(配列全体) / sizeof(要素1つ)」を計算すれば良いということなので、この計算式を関数形式マクロにしています。

問題③

問題③ 実引数で指定された2つの変数の値を交換する関数形式マクロを作成してください。


まず、2つの変数の値を交換するには、次のように書く必要があることを理解しなければなりません。

#include <stdio.h>

int main(void)
{
    int v1 = 10;
    int v2 = 20;
    int work;

    work = v1;    // 作業用の変数に退避
    v1 = v2;
    v2 = work;    // 作業用の変数から復元

    printf("%d %d\n", v1, v2);
}

実行結果:

20 10

代入という操作は、いわば上書きコピーなので、次のように書くとうまくいきません。

v1 = v2;
v2 = v1;

最初の代入が終わったとき、v1 の元の値は、上書きコピーによって失われています。そのため、さきほどのサンプルのように、作業用の変数を1つ用意して、そこへ退避させる必要があります。

では、これを関数形式マクロにできるでしょうか? 単純に書けば、次のようにできそうです。

#define SWAP(a, b)  work = a; \
                    a = b;    \
                    b = work;

この程度なら1行で書いても問題ないですが、改行したければ、この例のように、行末に \ を置いてください。

【C++プログラマー】C言語には標準の swap関数のようなものはありません。

この SWAPマクロを実際に使おうとすると問題に気付くことになります。

int main(void)
{
    int v1 = 10;
    int v2 = 20;
    SWAP(v1, v2);  // コンパイルエラー。work が宣言されていない
}

マクロの置換後の結果に含まれている変数 work が宣言されていないため、エラーになります。もちろん、マクロを使う側で宣言すればエラーは消えますが、マクロ側の実装で完結していないのは、あまり気の利いたマクロとはいえません。

int main(void)
{
    int v1 = 10;
    int v2 = 20;
    int work;
    SWAP(v1, v2);  // 一応 OK
}

マクロは単なるコードの置換なので、work の宣言を置換後の文字の並びに含めることは可能です。

#include <stdio.h>

#define SWAP(a, b)  int work = a; \
                    a = b;        \
                    b = work;

int main(void)
{
    int v1 = 10;
    int v2 = 20;
    SWAP(v1, v2);
    printf("%d %d\n", v1, v2);
}

実行結果:

20, 10

これは可能ですが、仮引数a、b には型の指定がないのに、work は int型に固定されることが勿体ないですし、実引数が double型の場合に、暗黙の型変換で int にされる事故が起こり得ます。また、マクロを使う側に、すでに別の目的で work という名前の変数が宣言されていたら、重複が起きてしまいます。

まず、型の問題は、型の指定もマクロに与えるようにすれば解決します。

#include <stdio.h>

#define SWAP(type, a, b)  type work = a; \
                          a = b;         \
                          b = work;

int main(void)
{
    int v1 = 10;
    int v2 = 20;
    SWAP(int, v1, v2);
    printf("%d %d\n", v1, v2);
}

実行結果:

20, 10

実引数に型名を記述するというのは、不思議な感じがしますが、マクロは文字の並びの単純な置換なので、このようなことも可能です。

次に、work の宣言が重複する可能性への対処です。これは、置換後のコードをブロックの中に閉じ込めることで解決できます。

#include <stdio.h>

#define SWAP(type, a, b)  { type work = a; \
                            a = b;         \
                            b = work; }

int main(void)
{
    char work[128];  // すでに別の用途の work がある
    int v1 = 10;
    int v2 = 20;

    SWAP(int, v1, v2);
    printf("%d %d\n", v1, v2);
}

実行結果:

20, 10

この場合も、work という名前の変数が隠蔽される(隠される)という意味の警告は出るかもしれませんが、エラーは防がれます。

基本的な考え方はここまでの時点でうまくいっていますが、まだ問題があります。次のように呼び出すとうまくいきません。

#include <stdio.h>

#define SWAP(type, a, b)  { type work = a; \
                            a = b;         \
                            b = work; }

int main(void)
{
    int v1 = 10;
    int v2 = 20;

    if (v1 != v2)
        SWAP(int, v1, v2);
    else
        puts("No need for swap.");

    printf("%d %d\n", v1, v2);
}

どこに問題があるかは、置換後の状態を考えれば分かります。

    if (v1 != v2)
        { int work = v1; 
          v1 = v2;
          v2 = work; };
    else
        puts("No need for swap.");

交換を行っている文が終わるとき、; があります。if文の真の場合のブロックの終わりと、elseキーワードの間に ; を置くことは、構文上許されません。

この問題を解決するために常套句として使われている手法があります。それは次のように、do文を利用することです。

#define SWAP(type, a, b)  do { type work = a; \
                               a = b;         \
                               b = work; } while(0)

マクロの置換結果を取り囲んでいたブロックを、do文のブロックに変えています。ループさせることが目的ではないので、ループを継続する条件式を 0 としておきます。そして、while(0) の後ろには ; はありません。

このような定義にすると、さきほどの問題があったマクロの呼び出し例は、次のように置換されることになります。

    if (v1 != v2)
        do { int work = v1; 
          v1 = v2;
          v2 = work; } while(0);
    else
        puts("No need for swap.");

問題だった ; は、do文の末尾の ; して機能しますから、余分な ; は一つもなく、正しい構文になりました。

もちろん、このような問題が起こるのは、if文で { } を省略していることにもあります。当サイトとしては { } の省略は推奨しませんが、省略して書くプログラマーも多くいます。マクロ定義側の工夫で対処できることなので、対処しておくほうが親切であるといえます。


参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



第28章のメインページへ

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る