char と int の変換キャストの留意点 - C++ プログラミング

PROGRAM


char と int の変換キャストの留意点

C 言語では文字列を char 型で扱いますが、大文字を小文字に変換する std::tolower 関数などは int 型で文字を受け取ったりします。

余談としては、同じ std::tolower でも <locale> ヘッダーに定義されている、第二引数に std::locale を取る関数の場合は char 型で文字を渡すことになりますけれど。

 

int と char が扱う範囲

そんな int と char ですけど、扱う値の範囲が違います。

たとえば Xcode 4.6.2 + iOS SDK 6.1 の場合は次のようになっています。

サイズ 最小値 最大値 注意事項
int 4 Bytes -2147483648 2147483647 処理系によっては 4 バイトとは限りません。
char 1 Byte -128 127 1 バイト辺りのビット数は処理系によって異なります。符号を持つかどうかも処理系によって異なります。

これらは処理系によって異なることになっていますが、最小値と最大値に関する情報は <limits.h> ヘッダーに定義されていて、判るようになっています。

int の最小値と最大値はそれぞれ INT_MIN と INT_MAX で、char の最小値と最大値はそれぞれ CHAR_MIN と CHAR_MAX で確認できます。また、int を構成するバイト数は sizeof(int) で確認できます。

char は 1 バイトの型ですけど、1 バイトが何ビットで構成されているかは処理系依存で、それは CHAR_BIT で確認できます。最低でも 8 ビットはあることは規格上保障されているようです。

 

char が符号を扱うかどうかで変わる影響

char が符号を扱うかどうかは処理系依存です。

構文解析などの処理でよく使う ASCII コードは 7 ビットで表現されているので、char が符号を扱おうと扱わなかろうと影響はまずありません。ただ UTF-8 などで 7 ビット以上の値で表現された文字の場合、符号の有無が大きく影響してきます。

 

char 型の符号の有無を調べるには 数値型で扱える値の最小値や最大値を取得する のところで記した std::numeric_limits<char>::is_signed でも知ることができますが、ここでは符号のあるなしを知らなかった場合に起こり得る影響について見て行きます。

 

大小関係への影響

たとえば 0xE7 という表現の文字コードがあったとき、8 ビットの char で表現できる値は、符号ありと符号なしとでそれぞれ次のようになります。

  0xE7
signed char -25
unsigned char 231

このように符号が違ってくると、大小関係も違ってきます。

たとえば 0x7F (127) と 0xE7 を比較したとき、符号つき char の場合は小さくなり、符号なし char の場合は大きくなります。

 

つまり、0x00 から 0x7F 以内に収まっているかを調べるとき、次の条件式では符号の有無によって結果が変わってしまいます。

if (value < 0x7F)

この場合については "if (value >= 0x00 && value <= 0x7F)" とすれば大丈夫ですけど、これが 0x00 から 0x90 までだったりすると、同様に "if (value >= 0x00 && value <= 0x90)" という条件では正しく判定できません。

 

char 型から int 型へ変換する際の影響

大小関係のときと同じですが、char 型の値を int 型へ格納したときに影響があります。

通常の int 型は 2 バイト以上だと思うので、char が符号を持てるかどうかにかかわらず、数値としては変化がありません。

  int 型へ代入した後の値
signed char -25 -25
unsigned char 231 231

ただ、これを 2 進数で表現させると、少しばかり様子が違ってきます。

  -25 231
signed char 11100111 11100111
unsigned char 11100111 11100111
int 11111111 11111111 11111111 11100111 00000000 00000000 00000000 11100111

このように char 型を int 型に変換すると、ビットによる表現が異なってくるので注意が必要です。

幸い、符号の扱いは上手に作られていて、int の最後 8 ビットと、符号がどうあれ char 型とでは 2 進数が同じ値になっているので、AND (&) や OR (|) と等号を使って値の比較を行う分には char の符号が結果に影響することはあまりないかもしれません。

 

それでも、たとえば char が符号なしと思い込んでいると、定数を使った判定時に思わぬミスにつながる可能性はあります。

char value = 0x7F;

 

if (value == 0x7F)

{

}

このとき、char 型が 8 ビットで符号を扱う場合は、条件が一致しません。

これは 0x7F と書いた表記が int 型として扱われるためで、value に代入したときに丸められて -25 になります。そして条件文では char 型の -25 と int 型の 231 (0x7F) とを比較します。

このとき C++ では int 型よりサイズの小さい型は int 型に変換されて比較されるらしいので、文字通り -25 と 231 は一致しません。

char 型が符号を扱わない場合は 231 になるので、条件式は一致します。

 

char 型が符号を扱うかどうかに関わらず正しく比較したい場合は、0x7F を比較対象と同じ char 型にキャストすることで、意図したとおりに比較ができます。

if (value == (char)0x7F)

{

}

ただしもちろん "(char)0x7F" は、signed char では "-25" に、unsigned char では "231" になるので、不等号を使った比較の場合は、大小関係が真逆になるので注意です。

 

int 型から char 型へ変換する際の影響

int 型を char 型に変換する場合は、int 型の全てを char が扱えるわけではないので、値が大きく変わる可能性があります。

char が符号を扱うときでは、8 ビットなら -128 から 127 までで int 型が表現されていた場合は同じ値に変換されますが、その範囲外にあるときは処理系によって変換後の値が異なってきます。

たとえば Xcode 4.6.2 + iOS SDK 6.1 では、0xE7 の値を符号付 char に変換すると、次のようになりました。

0xE7 変換前 (int) 変換後 (signed char) 注意事項
10 進数で表現 231 -25 int の値が signed char で表現できる範囲外の場合は、処理系によって結果が異なります。今回の場合は、入りきらなかったビットがそのまま無視されるようでした。
2 進数で表現 00000000 00000000 00000000 11100111 11100111

 

char が符号を扱わないときだと、8 ビットなら 0 から 255 までで int 型が表現されていた場合は同じ値に変換されますが、その範囲外にあるときは処理系によって変換後の値が異なってきます。

8 ビット unsigned char で表現できない 300 という値で見てみると、Xcode 4.6.2 + iOS SDK 6.1 では次のような感じになりました。

300 変換前 (int) 変換後 (unsigned char) 注意事項
10 進数で表現 300 44 int の値が unsigned char で表現できる範囲外の場合は、処理系によって結果が異なります。今回の場合は、入りきらなかったビットがそのまま無視されるようでした。
2 進数で表現 00000000 00000000 00000001 00101100 00101100

ただ今回は文字表現がテーマなので、unsigned char が表現できる範囲外の値を int 型の値が表現していないとみなしても、問題ないかもしれません。

 

シフト演算子への影響

char 型が符号を持つかどうか、そして int 型への変換で、シフト演算の結果が大きく影響されます。

まず、覚えておきたいところとして、int 型より小さい型のシフト演算は int 型に変換された上でシフト演算が行われます。両辺をそれぞれ char で統一していても int 型に変換されるため、これまでお話したような char 型から int 型への変換は避けて通れません。

■ 左ビットシフト (<<)

たとえば左シフトの場合、0xE7 という値を Xcode 4.6.2 + iOS SDK 6.1 環境で、符号付 char、符号なし char、int のそれぞれで実行してみると次のようになりました。

  0xE7 シフト時に int 型に自動変換されると 左 5 シフト後の値(int 型) 結果を元の型に
キャストすると
注意事項
signed char -25 -25 -800 -32 左に溢れた分は捨てられます。右側には 0 が補てんされます。符号が加味されるかは処理系に依ります。
11100111 11111111 11111111 11111111 11100111 11111111 11111111 11111100 11100000 11100000
unsigned char 231 231 7392 224
11100111 00000000 00000000 00000000 11100111 00000000 00000000 00011100 11100000 11100000
int 231 7392
00000000 00000000 00000000 11100111 00000000 00000000 00011100 11100000

このように、符号付 char、符号なし char、int のそれぞれで結果が大きく異なります。

ただ、int 型の末尾の 8 ビットは char 型と同じ値を示しているので、ビットを頼りに演算や比較を行う分には、左シフトはそれほど影響ないかもしれません。

実際のところ、左シフトでビット溢れが起こったときに符号を加味するかどうかは処理系に依存するらしく、例えば Xcode 4.6.2 + iOS SDK 6.1 環境であれば符号ビットも構わず左シフトする様子でしたが、いずれにしても char 型は int 型に変換されてからシフト処理が行われるため、char 型の範囲のビットに限れば、全て符号を無視して左シフトされると思っていても大丈夫かもしれません。

■ 右ビットシフト (>>)

そして、これが右シフトになるとまた少しややこしくなります。

たとえば 0xE7 という値を Xcode 4.6.2 + iOS SDK 6.1 環境で、符号付 char、符号なし char、int のそれぞれで右シフトしてみると次のようになりました。

  0xE7 シフト時に int 型に自動変換されると 右 4 シフト後の値(int 型) 結果を元の型に
キャストすると
注意事項
signed char -25 -25 -2 -2 右にあふれたビットは捨てられます。左側に何が補てんされるかは処理系依存です。
11100111 11111111 11111111 11111111 11100111 11111111 11111111 11111111 11111110 11111110
unsigned char 231 231 14 14
11100111 00000000 00000000 00000000 11100111 00000000 00000000 00000000 00001110 00001110
int 231 14
00000000 00000000 00000000 11100111 00000000 00000000 00000000 00001110

こちらは、符号付 char と符号なし char とで結果が大きく異なってきました。今回の場合は char 型だけで見ても、符号のありなしで 8 ビットの値が変わってくるので、char が符号付かどうかの違いに大きく影響をうけそうです。

ちなみに int 型も 255 よりも大きい数字のときには符号なし char と結果が変わってきます。

 

唯一、言えることとしては、捨てられもせず補てんされもせずに残ったビットだけは、char の符号の有無に関わらず同じ値というところでしょうか。

たとえば 8 ビットのうち 3 ビット右シフトしたとすれば、もともとの末尾 3 ビットが捨てられて、残りの 5 ビットがそのまま末尾にスライドした状態になっているので、その末尾 5 ビットだけを抽出すれば、char に符号があってもなくても、条件処理を適切に行うことができそうです。

 

ちなみに末尾の 5 ビットを抽出したい場合には、末尾 5 ビットだけが 1 にセットされた値を AND (&) で論理演算します。

つまり、2 進数で言えば 00011111 という値なので、10 進数では 31、16 進数では 0x1F です。

char x = ((value >> 5) & 0x1F);

たとえばこうすると、x には 残った 5 ビットだけの値が抽出されるので、符号付 char でも符号なし char でも int でも、それらの違いを気にせずに、同じ結果を得ることが可能になります。


[ もどる ]