【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 によるわけわからんバグは意外と多い。
気をつけよう。

*1:ついでに言うと、C言語で文字リテラル(シングルクォートで囲まれた一文字)を直接書いた場合、int 型になる。C++ではちゃんと char 型になる

*2:キャストすれば可能

*3:C言語では、int よりもサイズが小さい整数型は int に格上げされて処理されるため、引数や戻り値が char 型の関数は、標準Cライブラリにはそもそも存在しない(char へのポインタや、C++ では話は別だけど)