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( "%f\n", ABS(vald) );

    return 0;
}

実行結果:

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

このように ( ) で val を囲んでいないと、ABS(coli-30) の置換結果は、以下のようになります。

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

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

問題②

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


#include <stdio.h>

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

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

    printf( "%u\n", SIZE_OF_ARRAY(iArray) );
    printf( "%u\n", SIZE_OF_ARRAY(dArray) );

    return 0;
}

実行結果:

5
5

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

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

問題③

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


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

#include <stdio.h>

int main(void)
{
    int num1 = 10;
    int num2 = 20;
    int work;

    work = num1;    /* 作業用の変数に退避 */
    num1 = num2;
    num2 = work;    /* 作業用の変数から復元 */

    printf( "%d %d\n", num1, num2 );

    return 0;
}

実行結果:

20 10

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

num1 = num2;
num2 = num1;

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


そして本題ですが、これをマクロ化できるでしょうか? 作業用の変数を用意するにあたって、以下のような問題点があります。

これらの問題の解決を考えてみましょう。 次のようにマクロを定義します。

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

実際の使用例は次のようになります。

#include <stdio.h>

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

int main(void)
{
    int num1 = 10;
    int num2 = 20;

    SWAP(int, num1, num2);

    printf( "%d %d\n", num1, num2 );

    return 0;
}

実行結果:

20 10

まず、マクロの引数を使って型名も渡すようにしています。 実引数に型名を指定するという発想は、関数形式マクロの独特なものですが、これは可能です。

そして、置換後の結果全体を { } で囲むことによって、新しいブロックを作り出しています。 こうすれば、作業用変数を内側で宣言することで、そのブロックがスコープになるため、 他の変数や、他のところにも作られるかもしれない作業用変数と衝突しないようにできます。

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

#include <stdio.h>

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

int main(void)
{
    int num1 = 10;
    int num2 = 20;

    if( num1 != num2 )
        SWAP(int, num1, num2);
    else
        puts( "交換の必要なし!" );

    printf( "%d %d\n", num1, num2 );

    return 0;
}

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

if( num1 != num2 )
    { int work = num1; num1 = num2; num2 = work; };
else
    puts( "交換の必要なし!" );

交換を行っている行の末尾に ; が付いていることが問題です。 if文の真の場合のブロックの終わりと、elseキーワードの間にセミコロンを置くことは、構文上許されません。

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

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

これを初めてみると、本当に不思議な感じがすると思いますが、こうやって、ブロックを do文のブロックにしてしまうことが解決手段です。 while(0) の後ろにはセミコロンはありません。 こう定義すると、先ほどの問題のあったマクロの呼び出し例は、次のように置換されることになります。

if( num1 != num2 )
    do{ int work = num1; num1 = num2; num2 = work; }while(0);
else
    puts( "交換の必要なし!" );

問題だったセミコロンは、do-while を終わらせるためのセミコロンとして機能しますから、構文上も許されるものになります。

もちろん、if文で { } を省略する行為にも問題があり、当サイトとしては { } の省略は決して推奨しませんが、 現実には、省略して書くプログラマはたくさんいます。 少しの工夫で対処できることは対処するべきです。

問題④

問題④ 次のプログラムはコンパイル可能ですか? 可能であるとしたら、どのような実行結果になりますか?

#include <stdio.h>

#define CALC(a,b,op)		a ## op ## b
#define CALC_STR(a,b,op)	#a ## #op ## #b

int main(void)
{
    printf( "%s=%d\n", CALC_STR( 10, 2, + ), CALC( 10, 2, + ) );
    printf( "%s=%d\n", CALC_STR( 10, 2, - ), CALC( 10, 2, - ) );

    return 0;
}


マクロを置換した結果は、次のようになります。

#include <stdio.h>

#define CALC(a,b,op)		a ## op ## b
#define CALC_STR(a,b,op)	#a ## #op ## #b

int main(void)
{
    printf( "%s=%d\n", "10""+""2", 10+2 );
    printf( "%s=%d\n", "10""-""2", 10-2 );

    return 0;
}

例えば、「10」と「+」を ##演算子で連結して「10+」を得ようとしていますが、 「10+」という字句は、変数名や関数名など、何かの名前になっていません。 本編で解説したとおり、##演算子の連結結果が、有効な字句になっていない場合の動作は未定義です。 そのため、このプログラムがコンパイルできるかどうかは、コンパイラによります。 例えば、VisualStudio ではコンパイルできますが、clang ではエラーになります。


参考リンク



更新履歴

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/3/4 全面的に文章を見直し、修正を行った。
章のタイトルを「マクロの活用」から「関数形式マクロ」に変更。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2018/2/21 文章中の表記を統一(bit、Byte -> ビット、バイト)

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



第28章のメインページへ

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

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー