初級C言語Q&A(12)

初出: C MAGAZINE 1996年5月号
Updated: 1996-03-12

[1つ前] [1つ後] [質問一覧] [記事一覧] [ホームページ]


ビット操作、論理演算


C言語は低レベルの高級言語といわれますが(?)、理由の一つとして、シフトや マスクといったビット操作が比較的簡単に実現していることがあげられます。ア センブラから言語を学んだ場合は、これらの操作はごく簡単なのですが、高級言 語からいきなりプログラミングを始めた人には、ビット操作という概念そのもの に慣れていないため、分かりにくいかもしれません。

ビット操作


Q 【ビット操作】

 そもそも、ビットとは何なのか。

 コンピュータの中では、数値は0と1という二つの値で表現されています。たと えば、0と1のいずれかを入れることができる箱があると考えてみてください。こ れだけでは1と0の2通りの値しか表現できませんが、この箱をいくつか集めて一つ の数値だと考えれば、大きな数も表現することができます。

 例えば、このような箱が32個あった場合、それぞれの箱の中に0または1のどち らが入りますから、0と1の入れ方を変えることによって表現できる(区別できる) 種類は、2の32乗、すなわち4294967296通りあります。これはまさに32ビットの符 号なし整数が0〜4295967295を表現できるという現象に対応しています。

 ビットとは、このそれぞれの箱のことをいいます。従って、各ビットは0あるい は1という二通りの値のいずれかとなります。

    ┌─┬─┬─┬─┬─┬─┬─┬─┬〜┬─┬─┬─┬─┐
    │0│1│0│1│0│1│0│1│ │0│1│0│1│
    └─┴─┴─┴─┴─┴─┴─┴─┴〜┴─┴─┴─┴─┘
      ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ 〜 ↑ ↑ ↑ ↑
             それぞれが、ビット

Q 【ビットの検査】

 あるビットが1か0かを知るにはどうすればよいか。

 二項演算子の「 & 」を使います。例えば、下から3番目のビットを調べたい場合には、リストのよ うな操作を行います。1という値は一番下のビットが1ですから、この値を左に n-1ビットシフトすれば、n番目のビットだけが1の値を得ることができます。下 から3番目のビットだけ1の数は (1 << 2) ですから、この値との「 & 」を求めることにより、そのビットが1か0かを判断することができます。
    if (a & (1 << 2)) {
        /* 下から3番目のビットが1 */
    } else {
        /* 下から3番目のビットが0 */
    }

Q 【ビットの変更】

 あるビットだけを1か0に変更し、残りのビットを元のままにするにはどうすれ ばよいか。

 あるビットだけを1にするにはビット論理演算子の「 | 」を使います。1とのorの結果は常に1となります。そこで、目的のビットだけが 1である数とのorを求めれば、目的のビットだけを変更することができます。次の コードは、 a の値に対して下から7番目のビットを1に変更し、その結果を元の変数である a に代入します。目的のビットが最初から1の場合は、その値は変化しません。
  a |= (1 << 6);
 あるビットだけを0にするには、0との「 & 」は0であることを利用します。1とのandの結果は、元の値が0なら0、1なら1とな るため、値を変化しません。従って、目的のビットだけが0である数とのandを求 めれば、目的のビットだけを変更することができます。このような定数を得るた めに、「 」演算子を使います。この演算子は、対象とする値のビットを全て反転します。 すなわち、1であるビットは0に、0であるビットは1に変更します。

次のコードは、 a の値に対して下から7番目のビットを0に変更し、その結果を元の変数である a に代入します。目的のビットが最初から1の場合は、その値は変化しません。

  a &= ‾(1 << 6);

    1
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │0│0│0│0│0│0│0│0│0│1│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

    1 << 6 :左に6つシフトする
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │0│0│0│1│0│0│0│0│0│0│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

  ‾(1 << 6) :0と1を反転する
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │1│1│1│0│1│1│1│1│1│1│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘


      a  : aの値
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │J│I│H│G│F│E│D│C│B│A│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

  a & ‾(1 << 6)
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │J│I│H│0│F│E│D│C│B│A│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

Q 【ビットの変更】

 いくつかのビットは元の値のままで、残りのビットの値を目的の値に変更した い。どうすればよいか。

 このように複数のビットの値だけを変更したい場合は、まず「 & 」で変更したいビットを0にセットしておき、それから「 | 」を使って1にしたいビットを1にします。例えば、変数aの下位4ビット(下から数 えて1〜4番目のビット)を下からそれぞれ0、0、1、0という値にするには、
    a = (a & ‾0x0f) | 0x4;
 を実行します。
    0x0f
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │0│0│0│0│0│0│1│1│1│1│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

   ‾0x0f
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │1│1│1│1│1│1│0│0│0│0│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘


      a  : aの値
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │J│I│H│G│F│E│D│C│B│A│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

  a & ‾0x0f
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │J│I│H│0│F│E│0│0│0│0│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

     4
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │0│0│0│0│0│0│0│1│0│0│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

  (a & ‾0x0f) & 4
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │J│I│H│0│F│E│0│1│0│0│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

Q 【ビットの反転】

 特定のビットだけを反転したい。すなわち、そのビットが0なら1、1なら0にし たい。どうすればよいか。

 この操作は「 ^ 」という演算子を使って簡単に実現できます。例えば、下位2ビットだけを反転さ せるには、次のようにします。
    a ^= 3;

Q 【ビット演算子の評価順序】

 次のコードが思った通りに動作しないのはなぜか。
    if ((c1 = getchar()) & (c2 = getchar()) & 0x80) {
        /* EUC漢字コードの処理 */
    }

(c1 = getchar()) という式の値は、 c1 の値になります。 (c2 = getchar()) の値は c2 になります。従って、これらの値のandを計算し、さらに0x80という値とのandを 求めれば、両方の文字の8ビット目が1である場合を検出できる、と考えたのでし ょう。この発想は悪くありません。

 ところが、「 & 」という演算子は、その両辺の評価順序が不定であると決められています。従っ て、このコードは一見 (c1 = getchar()) が先に実行されるように見えますが、実は (c2 = getchar()) が先に実行されるかもしれないのです。そして、その場合には、この後に行われ る漢字コードの処理で、1バイト目と2バイト目が入れ替わってしまうため、思っ た通りの動作にならないのです。また、元の処理は EOF が戻ってきた場合を想定していないので、どこかでそれを検査する必要がありま すが、とりあえず元のコードが期待した動作をするように修正するには、次のよ うにすればよいでしょう。

    c1 = getchar();
    c2 = getchar();
    if (c1 & c2 & 0x80) {
        /* EUC漢字コードの処理 */
    }

Q 【1のビットの数】

 ある整数の値に含まれているビットが1の個所の数を素早く調べる方法は?

 一般的には、テーブルを参照するのが高速です。例えば、1バイト整数に含まれ る1のビットの数をテーブルにします。
    table[0] = 0;
    table[1] = 1;
    table[2] = 1;
       :
    table[254] = 7;
    table[255] = 8;
 これを使えば、32ビット整数aのビットが1の個数は、
    table[a & 0xff] + table[(a >> 8) & 0xff] +
      table[(a >> 16) & 0xff] + table[(a >> 24) & 0xff]
 で求めることができます。  もう一つの、割と有名なトリッキーな方法を紹介します。
/* 与えられたintの中の1であるビットの数を返す。
 * 最上位2ビットは利用できない
 */
int numofbits(int bits)
{
    int num;

    num = (bits >> 1) & 03333333333;
    num = bits - num - ((num >> 1) & 03333333333);
    num = ((num + (num >> 3)) & 0707070707) % 077;
    return num;
}

(参考: c.l.c FAQ 20.12)


シフト


Q 【シフト】

 シフトとは何か

 ある値をビットの並びとみたてた場合に、それらの並びの順序を変更せずに、 全体をずらしてやる操作のことをいいます。例えば、図の場合は、全体を右に一 つずらしたことになります。右にずらす方向のシフト操作を右シフト、左にずら す方向のシフトを左シフトと呼んでいます。
    右シフトの場合

      ┌─┬─┬─┬─┬─┬─┬─┬─┬〜┬─┬─┬─┬─┐
シフト前  │0│1│0│1│0│1│0│1│ │0│1│0│1│
      └─┴─┴─┴─┴─┴─┴─┴─┴〜┴─┴─┴─┴─┘
        \ \ \ \ \ \ \ \   \ \ \ \ 
         \ \ \ \ \ \ \   \ \ \ \ 消える
      ┌─┬─┬─┬─┬─┬─┬─┬─┬〜┬─┬─┬─┬─┐
シフト後  │?│0│1│0│1│0│1│0│ │1│0│1│0│
      └─┴─┴─┴─┴─┴─┴─┴─┴〜┴─┴─┴─┴─┘


    左シフトの場合

      ┌─┬─┬─┬─┬─┬─┬─┬─┬〜┬─┬─┬─┬─┐
シフト前  │0│1│0│1│0│1│0│1│ │0│1│0│1│
      └─┴─┴─┴─┴─┴─┴─┴─┴〜┴─┴─┴─┴─┘
       / / / / / / / / / / / / /
    消える / / / / / / / / / / / /
      ┌─┬─┬─┬─┬─┬─┬─┬─┬〜┬─┬─┬─┬─┐
シフト後  │0│1│0│1│0│1│0│ │1│0│1│0│?│
      └─┴─┴─┴─┴─┴─┴─┴─┴〜┴─┴─┴─┴─┘
C言語では、左シフトの演算子として「 << 」、右シフトの演算子として「 >> 」を用いることになっています。

このように、シフトした結果、一部のビットは外にはみでてしまうので、それら の情報は消滅することになります。逆に、シフトによってできた空きビットには、 何を入れるかということが問題になります。

C言語では、左シフトの場合、空いたビットには0を詰めることになっています。 右シフトの場合は、シフトする値が符号無し整数型の場合には、空いたビットに 0を詰めることになっています。シフトする値が符号付き整数の場合は、結果は処 理系定義となっています。すなわち、あるコンパイラは1を詰めるかもしれないし、 別のコンパイラは0を詰めるかもしれません。どちらになるかは、処理系のマニュ アルを見てください。


Q 【シフト演算子】

 このコードは値をシフトしているはずなのに、値が変化しない。
    printf("a = %d¥n", a);
    a << 3;
    printf("a = %d¥n", a);

 シフト演算子は2項を持つ演算子で、左辺の値を右辺の数値だけシフトした値を 式の値とします。「 a << 3 」という式は、 a の値を左に3つシフトさせた値を持ちますが、 a の値そのものは変化しません。 a の値を変えるには「 a = a << 3 」のように結果を代入するか、あるいは「 a <<= 3 」のように代入演算子とシフトを組み合わせたものを使います。

Q 【シフトによる演算】

 次のコードは、2バイトのリトルエンディアンで格納された整数値をファイルか ら読むものである。(実際のコードは EOF の処理があるため若干複雑だが、問題点とは無関係なので省いてある。)
    c = getc(fp);
    c += getc(fp) * 256;
 しかし、このコードは次のようにした方が処理が速いといわれた。本当か?
    c = getc(fp);
    c += getc(fp) << 8;

 多くのプロセッサにおいて、整数値のシフトという処理は単純な機械語の命令 に置き換えることができます。掛け算の命令を持っているプロセッサもあります が、最高速でもシフトと同程度で、シフトよりも遅い場合もあります。なぜ掛け 算の方がシフトより遅いかというと、掛け算という処理は内部でシフトを組み合 わせることによって実現されているからです。

 左に1回シフトするという処理の結果は、元の値を2倍することになります。従 って、8回シフトした結果は、2の8乗倍、すなわち256倍と同じになるのです。

 ということは、256を掛けるよりは、左に8シフトした方が処理が速くなるとい う結論でしょうか。残念ながら、その努力は報われない確率が極めて高いでしょ う。なぜなら、最近のコンパイラは昔に比べて随分賢くなったので、256を掛ける というコードがある場合には、もしそれを左シフトに置き換えた方が処理速度が 速いのであれば、勝手に置き換えるという程度の工夫は当然のごとく行うように なっているからです。従って、これが本当に速いコードになっているかどうかは、 コンパイルした結果を分析しなければ何とも言えません。

 一般論としては、掛け算で書くべき所をわざわざシフトに書き直すのは、コー ドを分かりにくくする原因となるので、避けるべきです。

 ただ、この例の場合は、整数値をファイルから1バイトずつ読み込んで組み合わ せるという処理のはずですから、むしろシフトで書いた方が意味が明確になると いう考え方もあります。悪くはありません。


Q 【ローテイト】

 ローテイトを実現する簡単な方法は?

C言語にはローテイトに対応する演算子はないので、シフトを組み合わせて実現 するか、非標準的なasm文を使うしかありません。


論理演算


Q 【論理演算】

 論理演算とは何か。

 真偽値を結果とする演算のことをいいます。具体的には、 &&||! の3つの演算子を使った演算のことです。

 ビット毎の演算子との違いは、論理演算の結果は、0か1のどちらかであるとい うことです。言いかえれば、論理演算とは、0は0、0以外の全ての値は1とみなし て1ビット整数のビット演算を行ったのと同じ結果を得ると考えてもよいでしょう。


Q 【!と‾の違い】

 単項演算子の「 ! 」と「 」はどこが違うのか。

 「 ! 」は、論理演算子の否定とされています。すなわち、その結果は0か1のどちらか となります。「 」はビット毎に反転を行います。結果は元の値の0と1を全て逆転させたものとな ります。例えば、1に対するそれぞれの演算子を反映させた結果は図の通りで、 違いは一目瞭然です。
    1
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │0│0│0│0│0│0│0│0│0│1│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

   !1
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │0│0│0│0│0│0│0│0│0│0│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

   ‾1
      〜┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
       │1│1│1│1│1│1│1│1│1│0│
      〜┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

Q 【論理演算子の評価順序】

 次のようなコードを見たことがある。評価順序の問題はないのか?
    if (((c1 = getchar()) >= 0x80) && ((c2 = getchar()) >= 0x80)) {
        /* EUC漢字コードの処理 */
    }

A  C言語のほとんどの演算子は評価順序が不定であるという仕様になっていますが、 少しだけ例外のものがあります。論理演算子はその例外であり、常に左から右に 評価されることが保証されています。

 従って、このコードの場合は、 && の左側にある ((c1 = getchar()) > 0x80) という式がまず評価されることが間違いないため、 c1c2 は期待した順序の値が入ることになります。

(参考) 【&&や||と副作用完了点】


(C) 1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。