char の落とし穴

C 言語の char 型や Java の byte 型は、文字列処理やデータの読み書きに無くてはならない存在です。でも、少し込み入ったことをしようとすると、いろいろと直感に反する挙動をしてしまいます。 ここでは char / byte を使った演算がどんな理屈で動いているか説明してみます。
(ここでは PC 環境を想定して、char は signed、int は 32bit で書いています。 あと、タイトルには "char の" と書きましたが、話は整数の型変換全てに適用できる話です)

char でハマるケーススタディ

char は -128 から +127 までを表せる型です。
でも、その知識だけだと色々と不思議なことが出てきます。

ケース1: その値、char に入れていい?

バイナリファイルを読み込む時、1 バイト単位の定数が欲しくなります。
そこで、以下のようなコードを書いてみました。
char c = 0xff;
あれ? char って -128 から 127 までですよね? 0xff って10 進数だと 255 だけど、代入していいんだっけ?

ケース2: 比較してみると...

さっき c に 0xff が代入できるか不安だったので、比較してみることにします。
int main(void){
  char c = 0xff;
  if(c == 0xff){
    printf("ok!\n");
  } else {
    printf("??\n");
  }
}
実行してみます。
??
c は 0xff を代入したはずなのに、c の値は 0xff とは異なるようです。
じゃあ c には何が入っているんだろう...

ケース3: ビット演算で謎の ff

4 バイトの int 値にバイト列を詰めます。既に 0x100 が入っていて、下位 1 バイトに c の 0x80 を詰めます。
int main(void){
  int target = 0x100;
  char c = 0x80;
  target = target | c;
  printf("target = 0x%x\n", target);
}
コンパイルして実行すると、結果はこうなります。
target = 0xffffff80
"0x180" になるかと思いきや、上位のビットが全部つぶれてしまって大惨事です。
この ffffff ってどっから来たの? もしやビット演算のバグ?

解説編

上の混乱が起こるのは、二つの原因があります。 以下、順番に説明します。

C で書いた整数値 (即値) は int

C で 100 (16 進数だと0x64) とか 0xff とか書いた数値は、全て int 型として解釈されます。 int 型は 4 バイト、つまり char の 4 倍のメモリを使って値を保持します。(32bit の場合)
一方char 型の変数に入った 100 は、1 バイトで表します。つまり、
char c = 100;
と書くと、char に int の値を代入していることになり、型の変換が起こっています。

(注: この絵は big endian っぽく描いてあります)

変数には「ビット表現」と「値」がある

コンピューターのメモリではすべての値は 0 か 1 のビットで表されています。 ビットを決まった数だけ束ねて、int や char といった変数が出来ています。
一方、int や char といった整数型には、その変数が意味する「値」があります。 これは型を問わず共通なものです。int の 100 と char の 100 ではビット表現は異なりますが、 意味する「値」は同一です。(いちごが 100 個ある、と言ったとき、いちごの最大値を意識する必要はないですよね)
コンピューターでは、型が異なっていても、値を維持して、日常使う算数がそのまま使えるようになっています。 だから、int の 100 と char の 100 ではビット表現は異なりますが、足し算や比較をするときには あまり意識せずに同じように取り扱うことができます。 下の例では、char に入った100 と int に入った 100 を比較していますが、結果は TRUE になります。

型変換ではなるべく値を維持するが、オーバーフローした場合はビット表現を優先する

先に、普通は char と int を混ぜて使っても値が維持されると書きましたが、 これは変数の有効な範囲でのみ有効です。
char は -128 から 127 までを表せる変数で、この範囲では値が維持されます。 以下に 100 と -1 の例を示します。

特に -1 のほうのビット表現は ff ff ff ff と ff のようにずいぶん違いますが、表している値はどちらも -1 なので無問題です。
では、範囲外の値、例えば 128 を代入するとどうなるでしょうか?

char の変数の値は -128 から 127 まで。そこに 128 が来たら、値は何にするのが適切でしょうか? とりあえず一番大きな 127 というアイディアも出そうですが、C ではこんなときに 値よりもビット表現が優先されます。 この場合は、一番下のバイトの 0x80 が代入されます。

因みに、0x80 を char で解釈した値は -128 になっています。 これをもう一度 int に代入した場合、今度は値が維持されるので、128 には戻らずに -128 のままになります。

謎解き編

以上の知識を使って、先ほどのハマりケースを謎解きしてみます。

ケース1: その値、char に入れていい?

これは、char が -128 から 127 までなのに、0xff を代入していいんだっけ? という疑問でした。
char c = 0xff;
まず、0xff は int で、値は 255 です。これは char で表せる最大値 127 を超えているので、 ビット表現の下位 1 バイト分の 0xff がそのまま代入されます。 なので、c にはビット表現では 1111 1111 (0xff) が代入され、値は -1 になります。

ケース2: 比較してみると...

これはケース1 の続きです。先ほどの説明で、c に入っている値は -1 だと分かっています。
int main(void){
  char c = 0xff;
  if(c == 0xff){
    printf("ok!\n");
  } else {
    printf("??\n");
  }
}
if(c == 0xff) の文で、右辺の 0xff は int 型で、値は 255 です。 一方、左辺の値は -1 です。-1 と 255 の比較なので、結果は FALSE で、?? が出力されます。

ケース3: ビット演算で謎の ff

これは、target の下位 1 バイトに 0x80 を詰めようとするのに、上位のバイトが ff で埋められてしまうケースでした。
int main(void){
  int target = 0x100;
  char c = 0x80;
  target = target | c;
  printf("target = 0x%x\n", target);
}
target = 0xffffff80
まず、c = 0x80 が実行されたとき、0x80 の値は 128 で char の最大値よりも大きいので、 c にはビット表現がそのまま代入されて、値は -128 になります。
次の文、
target = target | c;
が問題です。まず、| の演算が実行される前に、c は int に型変換されます。 c を int にすると、値の -128 が維持されて、0xffffff80 (値は -128) になります。
つまり、上記の文は
target = target | (-128);
と同値になります。その結果、最後に出力されるのは 0xffffff80 になってしまいます。 ビット演算のバグではありません。
(この挙動を符号拡張と言います)

ケース3 (符号拡張) の回避方法

ビット演算では unsigned char を使うことで、代入はすべてビット表現で説明できるようになります。
int target = 0x100;
unsigned char c = 0x80;
target = target | c;
printf("target = 0x%x\n", target);
あるいは、char のままでも 0xff と and を取ることで、「値が維持されてビットが化ける」問題を回避できます。
int target = 0x100;
char c = 0x80;
target = target | (0xff &  c);
printf("target = 0x%x\n", target);

char と unsigned char

char と unsigned char は、どちらも 8 ビットの型ですが、 表現できる範囲が正だけか負かで範囲が違います。 char に入れた値を unsigned char に入れたり、その逆をすると どうなるのでしょうか?
char c = 0xff;
unsigned char uc = c;
先ほどのルール、「型変換では値の維持を優先するが、オーバーフローした場合はビット表現が優先される」がここでも適用できます。 つまり、char と unsinged char の間の代入では、常にビット表現が維持されます。 うまく出来ていますね。

この関係は int と unsigned int の間でも同じです。

おまけ: char と unsigned char のビット表現

復習を兼ねて、char と unsigned char のビット表現を書き上げておきます。
ビット表現 16進でのビット表現 char unsigned char
0000 0000 0x00 0 0
0000 0001 0x01 1 1
... ... ... ...
0111 1111 0x7f 127 127
1000 0000 0x80 -128 128
1000 0001 0x81 -127 129
... ... ... ...
1111 1111 0xff -1 255
同じビット列でも、char と unsigned char で値が違うのがポイントです。
char では一番上のビットが立っているのが負数になります。