ビット単位の演算とシフト演算

こんにちは、めのんです!

昨日は1日お休みをいただきましたが、今日からは通常通り更新していきます。
でも、1日も欠かさずに更新するのは大変なので、これからもときどきはお休みをいただくと思います。

さて、今回は「ビット単位の演算」と「シフト演算」を解説します。
これらの演算はPHPにもありますので、軽く解説する程度にしますね。

ビット単位の演算

Cのビット単位の演算には3種類があります。
PHPと同じで、ビット単位の論理積(AND演算)、ビット単位の論理和、そしてビット単位の排他的論理和です。

演算子の記号もPHPと同じですし優先順位も同じと考えて問題ありません。
ですが、まったくPHPのものと同じかというと微妙な違いがあります。

オペランドの型

PHPと違ってCの暗黙の型変換は限定的です。
文字列から整数値に暗黙に変換されるといったことはCではあり得ません。

Cでは基本的に算術演算子と同じように振る舞います。
つまり、演算子の演算を行う前に通常の算術型変換が行われます。

両辺のオペランドに対して通常の算術型変換を行った結果、整数型ではない場合にはコンパイルエラーになります。
PHPではfloat型やstring型でも演算できてしまうので、この点は要注意です!

符号付き整数型の場合

Cのビット単位の演算は整数型であれば符号付きであってもできてしまいます。
ですが、その評価結果は処理系定義になります。

これはどういうことかというと、Cの符号付き整数型では、マイナスの値をどんな風に表現するかが処理系によって異なるということです。

具体的には次の3種類があります。

  • 2の補数表現
  • 1の補数表現
  • 符号と絶対値

おそらくみなさんが遭遇する処理系は最初の「2の補数」を使ったものだと思います。
PHPもマイナスの表現は2の補数を使うことを前提としているようです。
ですが、Cの規格上はさきほどの3種類のどれかになります。

ちなみに私も2の補数以外の表現方法を使っている処理系は見たことがありません。

シフト演算

シフト演算もPHPにありますので、概要の説明は必要ないと思います。
Cのシフト演算にも左シフトと右シフトの2種類があり、演算子の記号や優先順位も同じと考えて問題ありません。

ですが、やはり微妙な違いがありますので、そうした違いに関してだけ解説しようと思います。

オペランドの型

オペランドの型は両辺ともに整数型でなければなりません。
これはビット単位の演算と同じですね。

気を付けないといけないのは、シフト演算では両辺は通常の算術型変換ではなくそれぞれ整数拡張が行われるということです。
たとえば

int a = 123;
long long b = 40LL;
long long c = a << b;

このような演算を行ったとき、通常の算術型変換によって両辺がlong long型になると思ったら間違いです。
それぞれについて整数拡張が行われますので、左辺はint型に、右辺はlong long型になります。

多くの処理系ではint型は40ビットもありませんから、こんなことをすれば未定義の動作になってしまいます。

左辺が符号付き整数型の場合

さきほど、int型の左辺を40ビットもシフトすると未定義の動作になると書きました。
実際、シフト演算では未定義の動作が起きやすいので、しっかりおさえておく必要があります。

右オペランド(シフト数)がマイナスの場合は問答無用で未定義に動作になります。
または右オペランド(シフト数)が左オペランドを整数拡張した型のビット数以上のとき未定義の動作になります。

あとは右シフトか左シフトかによって事情が変わってきます。

まず左シフトから見ていきます。

E1 << E2

上のような演算を行った場合

$$E1 \times 2^{E2}$$

が評価結果の値になります。

ところが、評価結果の値がE1を整数拡張した型が符号付き整数型の場合、その表現範囲を超える場合には未定義の動作になってしまいます。

次に右シフトです。

E1 >> E2

上のような演算を行った場合

$$E1 \div 2^{E2}$$

が評価結果の値になります。

ここでE1がマイナスの値だった場合、結果は処理系定義になります。
理由は前述したように、Cではマイナスの値の表現方法が3種類あるからです。

多くの処理系では、いわゆる「算術シフト」が行われることになります。
これは何ビットシフトしても、その分だけ符号ビットがコピーされて新しい符号ビットになります。

たとえば、int型が32ビットで、マイナスの内部表現が2の補数の処理系では

int a = -15;     // 2進数で 1111 1111 1111 1111 1111 1111 1111 0001
int b = a >> 2;  // 2進数で 1111 1111 1111 1111 1111 1111 1111 1100

ということで-4になります(これはPHPと同じですね)。


今回の解説は以上となります。

もうCの言語仕様については主な内容は解説してしまった感があります。
残っているのはマニアックな仕様かPHPとほとんど変わらないものばかりになってしまいました。

そろそろ別の企画を立ち上げる時期なのかもしれませんね。