MMGamesロゴ  MMGames
Twitterシェアボタン  Facebookシェアボタン   
しんで覚えるC言語
しんで覚えるC言語

SWAPマクロ

SWAPマクロ
SWAPとは、2つの変数の値を交換するマクロや関数です。
この関数そのものはとても簡単なことなのですが、逆にそれ故に、
マクロだけで実現できないかと考える人も多く、結構話題になっています。

まず、もっとも基本的な関数による実装から説明を始めます。
2つ変数の値を変更する必要があるので、当然ポインタ型の引数を使います。
後は、関数の側で2つの変数を交換すればよいのですが、ここでの注意点は、

ソースコード
void swap(int* a, int* b)
{
    *a = *b;
    *b = *a;
    return;
}

という方法では駄目だということです。
なぜなら、*aに*bを代入した時点で、*aの中身は*bの中身と同じになっているので、
そのあとで*aを*bに代入しても無意味だからです。

したがって、もう1つローカル変数を宣言し、そちらに*aを退避しておく必要があります。
次のプログラムは、もっとも標準的なswap関数の実装例です。

ソースコード
#include <stdio.h>

void swap(int* a, int* b);

int main(void)
{
    int a = 10, b = 100;
    printf("a = %3d : b = %3d\n", a, b);
    swap(&a, &b);
    printf("a = %3d : b = %3d\n", a, b);
    return 0;
}

void swap(int* a, int* b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
    return;
}

このプログラムの実行結果は、次の通りです。

実行結果
a = 10 : b = 100
a = 100 : b = 10

後は、これを型ごとに用意すれば良いだけなのですが、
C言語では、インラインやオーバーロードやテンプレートのような機能は使えませんので、
型ごとに別々の名前を付けて1つ1つ作ってやらなくてはなりません。
それでは使いにくいし、たとえC++でも、呼び出しのオーバーヘッドが気になるので、
何とかマクロだけで実現したいという人々は結構いるようです。

まず考えられたのは、次のような加減算による実装です。

ソースコード
#define SWAP(a, b) (a += b, b = a - b, a -= b)

原理は簡単ですね。先にbを足しておいて、bの値を保存するやり方です。
一見すると、足し算によるオーバーフローが気になりますが、
オーバーフローした場合でも、足した分はループするので、問題なく動作するわけです。

もうひとつ、排他的論理和を利用した方法も考え出されました。

ソースコード
#define SWAP(a, b) (a ^= b, b = a ^ b, a ^= b)

排他的論理和のついての説明は省きますが、原理的には大差ありません。
一般に、加算や減算より排他的論理和のほうがわずかに高速なので、場合によってはこちらを使います。
ただし、実数値には使用できないので、汎用性には欠けることになります。

この2つのマクロは、ほとんどの場合には問題なく動作するのですが、
実は、うまく動作しないケースがあり、それが最大の問題となります。
それは、同じ変数を指定した場合に起こります。
論より証拠なので、実際に試してみることにします。

ソースコード
#include <stdio.h>

#define SWAP(a, b) (a += b, b = a - b, a -= b)

int main(void)
{
    int a = 98;
    printf("a = %3d\n", a);
    SWAP(a, a);
    printf("a = %3d\n", a);
    return 0;
}

このプログラムの実行結果は、次の通りです。

実行結果
a = 98
a = 0

同じ変数であるaの値を交換したなら、結果は当然そのままなはずですが、
この方法では0になってしまっています。
この方法では、マクロのaとbは常に同じ値なので、引き算すれば0になるのは当然です。
ちなみに、排他的論理和による方法でもやはり0になります。

同じ変数を交換することなんてないと思うかもしれませんが、
実際には、ソートプログラムで、array[i] と array[j] を交換する時に i == j だった、
ということはありえます。しかも、SWAPの使い道のほとんどはソートプログラムです。

この問題はどうしようもないので、一般的にはこれ以上踏み込んではいないようなのですが、
筆者はこれも無理にでも解決したくて、&&演算子による方法を使っています。
&&演算子では、直前に実行した式が偽なら、後の式は実行しない性質があります。
次のマクロは、&&演算子の性質を利用したSWAPマクロです。

ソースコード
#define SWAP(a, b) ((a != b) && (a += b, b = a - b, a -= b))

このマクロでは、aとbの値が異なるときだけ、交換を実行します。
つまり、aとbが同じ変数だった場合には、交換されずにそのまま残ります。
ただ、こんなことをするよりは関数にした方が簡単だとも言えるかもしれません。

あるいは、演算子の性質に依存したくない場合は、3項演算子を使うこともできます。

ソースコード
#define SWAP(a, b) ((a != b) ? (a += b, b = a - b, a -= b) : 0)

後半の0は、文法のつじつま合わせのためのダミー値にすきません。
ただし、コンパイラによっては無意味な式として警告になるかもしれません。
これらのマクロであれば、どんな型(実数でも)の変数でも交換可能なのですが、
唯一、加減算のできないポインタ型の変数だけは交換できません

ポインタ変数を使用しない場合ならSWAPマクロを作ることができますが、
ポインタ変数の交換だけは、どうしても最初に示した関数による実装しかありません。
そして困ったことに、ソートプログラムでポインタを交換することもあるのです。

さらに、実数値を交換するときに、ケタ落ちが起こる可能性があります。
たとえば、変数aの値がものすごく巨大で、bの値がものすごく極小だった場合、
a+bを計算しても、結果が丸められ、aと同じになってしまう可能性があります。
したがって、正確さが求められる場合には使用できません。

結局、確実に値を交換するためには、どうしても一時変数が必要になってきます。
そのためには、マクロ内で変数を宣言する必要があります。

マクロは単なる置き換えに過ぎないので、型を指定すれば変数を宣言できます。
また、{} で囲んでブロック化すれば、変数名の衝突を避けることができます。
具体的には、次のようになります。

ソースコード
#define SWAP(type,a,b) { type temp = a; a = b; b = temp; }

このマクロは、初めの引数に型を指定する必要がある分、使い勝手は多少劣りますが、
型さえ指定すればどんな変数でも交換できます。
もちろん、実数値の交換やポインタ値の交換も可能です。

次のプログラムは、このマクロを使用して変数を交換する例です。

ソースコード
#include <stdio.h>

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

int main(void)
{
    int a = 10,b = 100;
    printf("a = %3d : b = %3d\n",a,b);
    SWAP(int,a,b)
    printf("a = %3d : b = %3d\n",a,b);
    return 0;
}

このプログラムの実行結果は、次の通りです。

実行結果
a = 10 : b = 100a = 100 : b = 10

もちろん、同じ変数を指定した場合にも問題なく動作します。
この方式では変数宣言のために{}で囲んでいるため、
;をつけなくても文として完結しています。
最後に;はつけないのが良いでしょう。

最後に;をつけたいなら
最後に;をつけられないのは気持ち悪いという人は
#define swap(type,a,b) do{type _c;_c=a;a=b;b=_c;}while(0)
としてやればマクロだけでは文として完結しないので、
最後に;をつけて動作するようになります。



本サイトについて

苦しんで覚えるC言語(苦C)は
C言語入門サイトの決定版です。
C言語の基本機能を体系立てて解説しており、
市販書籍と同等以上の完成度です。

第0部:プログラム概要編
  1. プログラムとは何か?
2章:プログラムの書き方
  1. 書き方のルール
  2. 書き方の慣習
  3. 練習問題2
3章:画面への表示
  1. 文字列の表示
  2. 改行文字
  3. 練習問題3
6章:キーボードからの入力
  1. 入力用の関数
  2. 入力の恐怖
  3. 練習問題6
9章:回数が決まっている繰り返し
  1. 繰り返しを行う文
  2. ループ動作の仕組み
  3. 練習問題9
10章:回数がわからない繰り返し
  1. 回数不明ループ
  2. 入力チェック
  3. 練習問題10
13章:複数の変数を一括して扱う
  1. 複数の変数をまとめて扱う
  2. 配列の使い方
  3. 練習問題13
20章:複数のソースファイル
  1. 最小限の分割
  2. 分割の定石
  3. 練習問題20

コメント
COMMENT

💬 コメント投稿欄を開く