【C/C++】char型の話
せっかくブログを開設したので、なんか書いてみようと思います。
以前どっかで書いた内容と大体同じだけど。
char 型とは
C/C++ で文字を表す整数型。文字"列"ではないことに注意が必要。
char なんていう名前の癖に、なぜ整数型に分類されるかといえば、ここで言う『文字』とは ASCIIコード(=数値)のことを指すからだ。*1
char、signed char、unsigned char
実は、上記の3つの型は、すべて違う型として扱われる。
なんと、 char 型と signed char 型は同じ型ではない。
そのため、C++ではこの3つの型でのオーバロードができてしまう。
void func(char c) { ... }
void func(signed char c) { ... } // 上の func() とは別の関数として定義ができる
void func(unsigned char c) { ... } // これももちろんオーバロード可能
テンプレートを使うときにも、char を期待しているのに signed char を渡したりすると、うまく引っかかってくれなくて、わかりにくいバグの原因になったりもする。
また、C++では異なる型へのポインタ間の代入は禁止されているため*2、次のようなコードはエラーになる(C言語では通ることは通るけど、コンパイラによっては警告が出る)。
char c;
signed char *p = &c; // エラー
char 以外の整数型は、signed をつけようがつけまいが型としては同じ。
void func(int n) { ... }
void func(signed int n) { ... } // 多重定義エラーになる
int n;
signed int *p = &n; // OK
char 型の符号のありなしは、規格では『処理系定義』とされている(要するにどっちでもいいってこと)。
ASCIIコードは 0 ~ 127 なので、8ビットあれば、符合があろうがなかろうが、すべてのASCII文字を表すことができる。
大半のコンパイラは char 型を符号ありとしていると思うが、たとえ char 型が符号なしであったとしても、規格に違反しているわけではない。
そのため、signed char と char が別の型なのは至極当然の話なのだ。
signed の歴史
初期のC言語では、 signed キーワードは存在しなかったらしい。
signed が加わった理由は、ひとつは、符号つきの型であることを明示したかったということ、もうひとつは、1バイトの符号あり整数を確実に定義できるようにしたかったということ。
char の符号のありなしが処理系定義であるおかげで、単純に char と書いても、必ずしも符号ありにはならない。
確実に符号ありにするために、それを明示するキーワードが必要だったわけだ。
たかが char 型と思って侮るなかれ
たとえば、EOF は -1 として定義されていると思う。
そして、fgetc() などは、ファイルの終端に来ると EOF を返す。
もし、戻り値の型が char 型で、なおかつ符合なしの char 型だった場合、 EOF を -1 として返すことができなくなってしまう。
そのため、このような関数の戻り値はすべて int 型として定義されている。*3
また、下記のようなコードは、無限ループになる可能性がある。
char c;
for (c = 20; c >= 0; c--) {
// なんか処理
}
char、signed char、unsigned char によるわけわからんバグは意外と多い。
気をつけよう。