[an error occurred while processing this directive]
[an error occurred while processing this directive]
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 なので、int と char の型変換(キャスト)が起こっている
- 変数には「ビット表現」と「値」がある
- 型変換ではなるべく値を維持するが、オーバーフローした場合はビット表現を維持する
以下、順番に説明します。
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 c = 0xff を実行ですが、0xff の int での値は 255 なので、char への代入では値ではなくビット表現が用いられて、c の値は -1 (char での 0xff) になります。
- 二行目の unsigned char uc = c; ですが、c の値は -1、ビット表現は 0xff です。一方 unsigned char は 0 から 255 までで、-1 は入られいないので、再びビット表現で代入されて、uc には 0xff が代入されます。
つまり、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 では一番上のビットが立っているのが負数になります。
[an error occurred while processing this directive]