C
C++

char の符号

More than 3 years have passed since last update.

プログラミング言語 C や C++ では、 char という型は符号有りか無しかは処理系定義とされています。 符号が有るか無いかのどちらかではありますが、どちらを選択するかは処理系に任せられていて規格では決めないということです。 また、 intsigned int (符号付き int) は同じ型ですが、 charsigned char とも unsigned char とも違う型です。 このことを知らないと、 C++ のオーバーロードの機能を使うときなどに予想外の結果をもたらすかもしれません。

具体的な事例を見てみましょう。 まずは int の場合、以下のコードをコンパイルしようとするとエラーになるはずです。 intsigned 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 の場合は charsigned 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-charchar が符号有りか無しかを切替えられるので、それぞれの場合で試してみてください。 (参考:GCC のドキュメント)

大小を比較するような場面でも符号の有無が結果を左右することもあります。 このあたりの数値の扱いは暗黙の変換で適当に良いようにしてくれることがあるのですが、一旦問題が発生するとわかり難いバグにもなるので「数値としての char 型」が必要なときは signed charunsigned char を陽に利用するのが無難です。 更に言えば int8_tuint8_t を使うのが近年の風潮です。

歴史的経緯で奇妙な部分を多くかかえている C/C++ ですが、最近の改訂でより良い方法が提供されていることがあるので、特に古い処理系に制約される環境でない限り新しい機能を積極的に活用することで馬鹿げたバグを防げるかもしれません。