#はじめに
C言語学習者にとっては誰もが一度は疑問に思う、**charとunsigned charとsigned charの使い分けがよくわからないよ!**という悩み。
ことの発端は、memcpyやmemcmp, memsetなどの関数のなかでは、汎用ポインタ(void*)型として渡された引数をunsigned char*型にコピーして操作しているらしい、ということに気づいたところから始まる。
void *memset(void *dst, int val, size_t len)
{
unsigned char *ptr = dst; //unsigned char*型を使用している!
while (len-- > 0)
*ptr++ = val;
return dst;
}
※memset : dstに対してlenバイト分だけvalで埋めるための関数。
このときに、「え?なんでunsigned char型なの?char型じゃダメなの?」と疑問に思ったので、いろいろ調べてみた。
#char型には3種類あり、すべて別物
まず、そもそもchar
とunsigned char
とsigned char
はすべて別物だ。
これこそが初学者が最も陥りやすい第一ポイントではないかと思う。
処理系は、char を、signed char または unsigned char のいずれかと同じ値の範囲、同じ表現形式、同じ動作をするものとして定義しなければならない。char はどちらに定義されたとしても、signed char とも unsigned char とも異なる型であり、これらの型と互換性はない。
(引用:JPCERT GC)
つまり、charをsinged- かunsigned- とするかは標準として未規定であり、これは処理系(コンパイラ)が定義するように任されている。そのどちらに定義されたとしても、この3種類の型に互換性はないということらしい。
だから、ネットでたまに「char型の範囲は-128〜127だ」と断言しているのを見かけるがそれは誤りである。
この辺はint
と仕様が異なるところなので注意したい。
データ型名 | バイト | その他の名前 | 値の範囲 |
---|---|---|---|
int | 2または4 | signed | -2,147,483,648 ~ 2,147,483,64 |
unsigned int | 2または4 | unsigned | 0 ~ 4,294,967,295 |
char | 1 | - | 0 〜 255 / -128 〜 127 |
signed char | 1 | - | -128 〜 127 |
unsigned char | 1 | - | 0 〜 255 |
参考:データ型の範囲 - Microsoft Docs |
#3種類のcharの基本的な使い分け
結論からいえば、文字集合としての単純な文字データを扱う場合にはcharを、数値として扱う場合にはsigned charかunsigned charを用いるのが基本的な使い分けである。
特に、mem-系の関数のように、操作対象が汎用型(void *など)で、その対象すべてのビットにアクセスしたいような場合にはunsigned charが積極的に使われている(後述)。
1. 文字集合を扱う場合
文字列処理系の標準ライブラリの仕様を考慮し、文字データを単純に表す際にはcharを使用するのが好ましい。
2. charを数値として扱う場合
この場合はsingned charかunsigned charを使う。
いずれにせよ値の範囲に収まるように注意せねばならず、特にsigned charの範囲は-128 ~ 127と狭いのでいまいち使いどきがピンとこない。
こういうときこそsigned charの出番だよ!という良い事例ないかな...
#unsigned charの特徴
冒頭で、mem-系の関数のなかでは汎用ポインタ(void*)型として渡された引数をunsigned char*型にコピーして操作しているのなんで?という疑問を紹介した。
詳しく調べてみると、どうやらunsigned charの標準仕様となっている表記法に関係があるようだ。
C標準[ISO/IEC 9899:2011]6.2.6.
unsigned char型の[...]オブジェクトに格納される値は純2進表記法で表現される。
...
(純2進表記法とは) バイナリの桁 0 および 1 を使用する整数の位置表現で、連続するビットで表現される値は加算方式をとり、1 から始まり、最上位ビット以外は連続する 2 の整数乗で乗算される。
だから、汎用ポインタ(void *)型などで渡されたオブジェクトの非ビットフィールドに対して、その表現を1バイトずつ検査することが可能になる。
...文系には頭がなかなか追いつかないが、unsigned char型のオブジェクトはパディングビットを持たないことがあるので、mem-系関数のようにメモリ領域そのものを扱うような関数で、操作対象の非ビットフィールドも含めたすべてのビットにアクセスしたいときにはunsigned char型の表記法が最も適する、ということのようだ。
memcpyやmemcmpなどの関数のなかで、void *型で渡される引数をunsigned char型にコピーして操作するのはそのためである。
unsigned charを完璧に理解するにはまだ道のりが長そう。。