Cで生メモリを触るとき、なぜ `unsigned char *` を使うのか
カテゴリー:技術記事
タグ:C言語
作成日時:
更新日時:
memset を自作していて、最初にこう書いた。
void *my_memset(void *s, int c, size_t n)
{
size_t i;
i = 0;
while (i < n)
s[i++] = c;
return (s);
}
しかし、標準Cでは void * に対して s[i] と書くことはできない。
void にはサイズがないため、
s[i]
すなわち
*(s + i)
としたとき、s + 1 がどれだけ先を指すのか決められないからだ。
そこで memset では、void * を unsigned char * として扱う。
void *my_memset(void *s, int c, size_t n)
{
unsigned char *p;
size_t i;
p = (unsigned char *)s;
i = 0;
while (i < n)
p[i++] = (unsigned char)c;
return (s);
}
なぜ unsigned char * なのか
理由は、Cにおける byteの定義にある。
Cの1 byteは、必ずしも8 bitではない。
1 byteを構成するbit数は処理系依存であり、一方で
sizeof(char) == 1
sizeof(signed char) == 1
sizeof(unsigned char) == 1
は常に成り立つ。
つまり、Cにおけるbyteは「8 bit固定の単位」ではなく、character type 1個分の記憶単位である。
そのため uint8_t は「正確に8 bitの符号なし整数型」であって、Cにおけるbyteそのものを意味する型ではない。
unsigned char と生のメモリ
さらにC標準では、任意のオブジェクトのメモリ上の表現を object representation と呼び、これを unsigned char の配列として扱えることが明記されている。
しかも、その説明ではコピー方法の例として memcpy が直接挙げられている。
つまり、
unsigned char *p;
p = (unsigned char *)s;
とすることで、
p[0]
p[1]
p[2]
のように、元のオブジェクトの型に関係なく、そのメモリ表現を1 byteずつ辿ることができる。
ポインタについても、オブジェクトへのポインタをcharacter typeへのポインタへ変換すると先頭byteを指し、そのポインタをインクリメントすることで後続のbyteを順番に指す、とC標準で規定されている。
さらに、通常は異なる型を通したオブジェクトへのアクセスには制約があるが、character typeはその例外として明示されている。
unsigned char * が生メモリを扱う用途で使われるのは、単なる慣習ではない。
Cではcharacter typeが1 byteの大きさを持ち、character typeを通してオブジェクトのbyte列へアクセスでき、さらにobject representationを unsigned char の配列として扱えるよう仕様で定められているからである。
memset 自体の仕様も、引数 c を unsigned char に変換して各要素へ書き込むものとして定義されている。
Appendix: C標準の該当箇所
参照したのは以下のWG14公開ドラフト。
ISO/IEC 9899:2023, N3096 — Programming languages — C
§3.6 byte
byteを構成するbit数について、
“the number of which is implementation-defined”
と明記されている。
したがって、Cの1 byteが常に8 bitであるとは限らない。
§6.5.3.4 sizeof
char、signed char、unsigned char に対する sizeof の結果は 1 と規定されている。
つまりcharacter type 1個分がCにおける1 byteとなる。
§6.2.6.1 Object representation
任意のオブジェクトの値は、同じサイズの unsigned char 配列へコピーでき、その結果のbyte列を object representation と呼ぶと規定されている。
ここではコピー方法の例として、
“e.g., by memcpy”
と明記されている。
memcpy のような関数が、型ではなくオブジェクトのbyte列そのものを扱うことを想定した規定になっている。
§6.3.2.3 Pointers
オブジェクトへのポインタをcharacter typeへのポインタへ変換した場合、先頭byteを指す。
さらに、
“yield pointers to the remaining bytes of the object”
とあり、インクリメントによって後続byteを順番に辿れることが明記されている。
§6.5 Expressions
オブジェクトへ格納された値を、通常とは異なる型のlvalueからアクセスすることには制約がある。
そのアクセス可能な型の一覧には、例外として a character type が明記されている。
これが、character typeを通してオブジェクトの生の表現へアクセスできる根拠になる。
§7.26.6.1 memset
memset は c を unsigned char に変換した値を、対象オブジェクトの先頭から n 個のcharacterへ書き込む関数として定義されている。
参考文献
- ISO/IEC JTC1/SC22/WG14 N3096, Programming languages — C
- §3.6
byte - §6.2.6.1 Representations of types
- §6.3.2.3 Pointers
- §6.5 Expressions
- §6.5.3.4 The
sizeofandalignofoperators - §7.26.6.1 The
memsetfunction
- §3.6