出力変換指定子の修飾子とか、整数拡張とか
仕事でビット演算を使って、結構知らないことが多かった。
まずは、signed charの場合。
#include <stdio.h> int main(void) { /* 以下いずれも右辺の定数はintで解釈される。右辺の値が左辺の型で表現できる範囲を超えている場合の動作は処理系定義。 */ signed char c = 0xf7; // 多くの処理系で0xf7は内部表現として解釈される。gccの場合、特にWarningは出ない。 c = 247; // これも同じ。gccの場合、特にWarningは出ない。 c = 0x123456f7; // 多くの処理系で下二桁が内部表現として代入される。gccの場合、overflow Warningが出る。 /* 出力指定子:size_t型はzで修飾。intが4バイトなら、charはhhで修飾。 */ printf("sizeof(c) = %zu, c = %hhd = %#hhx\n", sizeof(c), c, c); // 1, -9, 0xf7 /* signedの負数に対する右シフトは処理系定義。 */ c >>= 1; printf("c = %hhd = %#hhx\n", c, c); // 算術右シフトの処理系では、-5, 0xfb /* signedに対して論理右シフトさせたければ、キャストするかマスクをとる必要がある。 */ c = 0xf7; c = (unsigned char)c >> 1; printf("c = %hhd = %#hhx\n", c, c); // 123, 0x7b c = 0xf7; c = (c >> 1) & 0x7f; // 上位7ビットを読む。0x7f = 0b0111_1111 printf("c = %hhd = %#hhx\n", c, c); // 123, 0x7b return 0; }
次にunsigned charの場合。
#include <stdio.h> int main(void) { unsigned char uc = 0x12345680; // 右辺の値を(UCHAR_MAX+1)で除した剰余が代入されることが保証されている。gccの場合、overflow Warningが出る。 printf("sizeof(uc) = %zu, uc = %hhu = %#hhx\n", sizeof(uc), uc, uc); // 1, 128, 0x80 /* unsignedに対する右シフトは値にかかわらず論理右シフト(0埋め)。 */ uc >>= 1; printf("uc = %hhu = %#hhx\n", uc, uc); // 64, 0x40 /* しかし整数拡張(integer promotion)に嵌る可能性がある。 */ uc = ~uc >> 1; // 右辺のucはint型に暗黙にキャストされて、0x00_00_00_40となっている。 printf("uc = %hhu = %#hhx\n", uc, uc); // 223, 0xdf /* 回避するにはキャストするか、マスクをとる。 */ uc = 0x40; uc = (unsigned char)~uc >> 1; printf("uc = %hhu = %#hhx\n", uc, uc); // 95, 0x5f uc = 0x40; uc = (~uc >> 1) & 0x7f; // 上位7ビットを読む。0x7f = 0b0111_1111 printf("uc = %hhu = %#hhx\n", uc, uc); // 95, 0x5f return 0; }
教訓を列挙する。
・ビット演算を行う変数は、unsigned指定をつけたものに限る。なぜならば、型の表現できる範囲を超えた値が代入された時の挙動について、unsignedについては仕様によって定義されているが、signedの場合は処理系定義だから。左シフトやビット反転と整数拡張によって容易に型の表現できる範囲を超えてしまう。
・ビット演算を行った後は必ず、最後にマスクをかける。右シフトを行う場合、unsignedであっても整数拡張により1が埋められてしまうことがある。
・char型の変数をprintfでダンプする場合のフォーマット指定子にはhhをつける(int4バイトの場合)。
ややこしいことは考えず、この程度で面倒な問題を回避できるなら安いものである。