プログラミング言語 C や C++ では、 char
という型は符号有りか無しかは処理系定義とされています。 符号が有るか無いかのどちらかではありますが、どちらを選択するかは処理系に任せられていて規格では決めないということです。 また、 int
と signed int
(符号付き int
) は同じ型ですが、 char
は signed char
とも unsigned char
とも違う型です。 このことを知らないと、 C++ のオーバーロードの機能を使うときなどに予想外の結果をもたらすかもしれません。
具体的な事例を見てみましょう。 まずは int
の場合、以下のコードをコンパイルしようとするとエラーになるはずです。 int
と signed int
は同じ型なので定義が重複していることになるからです。
#include <iostream>
void foo(int x) {
std::cout << "int : " << x << std::endl;
}
void foo(signed int x) {
std::cout << "signed int : " << x << std::endl;
}
int main(void) {
int bar = 1;
foo(bar);
return 0;
}
しかし、 char
の場合は char
と signed char
、そして unsigned char
は別の型なので以下のようにオーバーロードしても問題なくコンパイルできますし、型に応じてそれぞれが呼び分けられます。
#include <iostream>
void foo(char x) {
std::cout << "char : " << x << std::endl;
}
void foo(signed char x) {
std::cout << "signed char : " << x << std::endl;
}
void foo(unsigned char x) {
std::cout << "unsigned char : " << x << std::endl;
}
int main(void) {
char bar = 'a';
signed char baz = 'b';
unsigned char qux = 'c';
foo(bar);
foo(baz);
foo(qux);
return 0;
}
また、 char
は符号が有りか無しかわからないのですから、負数を扱うときにその挙動が不確定となる場合があります。 たとえば以下のような場合です。
#ifdef __cplusplus
#include <cstdio>
using namespace std;
#else
#include <stdio.h>
#endif
int main(void) {
char x = -1;
printf("%d\n", x);
return 0;
}
MinGW の、少なくともバージョン 5.3.0 の gcc (または g++) ではこれは -1
を表示します。 MinGW GCC の char
はデフォルトでは符号有りなので汎整数昇格で int
に拡張されるときに符号がそのまま引き継がれて int
型の -1
になるからです。 しかし、 gcc の char
が符号付きか否かのデフォルトでの解釈はアーキテクチャによって異なるようです。 もし char
が符号無しと解釈されるなら上のコードは 255
を表示するでしょう。 gcc や clang では オプション -funsigned-char
-fsigned-char
で char
が符号有りか無しかを切替えられるので、それぞれの場合で試してみてください。 (参考:GCC のドキュメント)
大小を比較するような場面でも符号の有無が結果を左右することもあります。 このあたりの数値の扱いは暗黙の変換で適当に良いようにしてくれることがあるのですが、一旦問題が発生するとわかり難いバグにもなるので「数値としての char
型」が必要なときは signed char
か unsigned char
を陽に利用するのが無難です。 更に言えば int8_t
や uint8_t
を使うのが近年の風潮です。
歴史的経緯で奇妙な部分を多くかかえている C/C++ ですが、最近の改訂でより良い方法が提供されていることがあるので、特に古い処理系に制約される環境でない限り新しい機能を積極的に活用することで馬鹿げたバグを防げるかもしれません。