【汎整数拡張】第3回 汎整数拡張の意外な落とし穴

汎整数拡張、俗にいう「暗黙のキャスト」はソースコードに出てこないところの挙動なので、C言語上級者でもなかなかに引っかかりやすいトラップだと思います。

先日、組込み系の記事で見かけた例ですが…

以下コードは、unsigned char型変数をビット反転し、その上位4ビットを取得する(ことを意図した)ものです。
元の値は0x7F(0111 1111)なので、ビット反転すると0x80(1000 0000)となり、これの上位4ビットを取得すると、0x08(0000 1000)となる…はずです。

  5     unsigned char uc = 0x7F;
  6
  7     /* ビット反転し、上位4ビットの値を取得 */
  8     printf("uc=0x%x\n", uc);
  9     uc = ~uc>>4;
 10
 11     printf("uc=0x%x\n", uc);

しかし実行結果は…

before : uc=0x7f
after  : uc=0xf8

「0x08」とはならず、「0xF8」となりました。これは、汎整数拡張によるトラップです。
ビット単位否定演算子~がついたことで、ucは式中で評価されるため、int型に型昇格されます。あとはもう…お分かりですね?

1ステップずつ追っていきます。

元の値の内部表現です。unsigned char型なので8ビットです。

> unsigned char : 0x7F
0111 1111

次が問題なのですが、式「~uc」評価時に、内部表現がなんと32ビットに伸長しています。

> ~uc
1111 1111 1111 1111 1111 1111 1000 0000

これは、式中のucがint型に型昇格したことで「0x7F」→「0x0000007F」と伸長したためです。この時点では空きビットが0詰めされているのですが、それがビット反転により「0xFFFFFF80」となります。

> ~uc>>4
1111 1111 1111 1111 1111 1111 1111 1000

「0xFFFFFF80」を4ビット右シフトしたのですが、算術シフトにより、上位の空きビットは符号ビットの1で埋められます(算術シフト、論理シフトについては、別の回でまとめる予定です)。

最後に、unsigned char型変数への代入時、2^8で割った剰余が代入されるため、ucの値は「0xF8」となります。

> uc = ~uc>>4
1111 1000

以上、おわかりいただけただろうか。これが、汎整数拡張の恐ろしいところなんですよ。

これが

> unsigned char : 0x7F
0111 1111

こうなって(unsigned char→intへの汎整数拡張)

> uc
0000 0000 0000 0000 0000 0000 0111 1111

ああなって(ビット反転)

> ~uc
1111 1111 1111 1111 1111 1111 1000 0000

こうなって(int型なので算術右シフト。空いたビットは符号ビットと同じ1で埋められる)

> ~uc>>4
1111 1111 1111 1111 1111 1111 1111 1000

こうなるわけです。

> uc=~uc>>4
1111 1000

おわかりいただけただろうか。

おそらく、このコードを書いた人は「unsigned charに対する演算なので、7Fのビット反転で最上位ビットが1になったとしても、その4ビット右シフトは論理シフトとなるため、上位の空きビットは0で埋められるはず」と考えたのだと思います。実際は、ビット反転が行われる直前にunsigned char型→int型という型昇格が発生したため、上位の空きビットは0詰めされました。それをビット反転した結果、上位の空きビットはすべて1となったわけです。対策としては、ビットマスクをかけるなどしましょう。