strncmp を実装する際に困ったこと
突然ですが、以下の関数は strncmp
と同様の挙動をするように実装した関数です。
ただし、明確な誤りがあります。
その誤りはどこでしょうか?
#include <stddef.h>
int my_strncmp(const char *s1, const char *s2, size_t n)
{
size_t i;
i = 0;
while (i < n)
{
if (s1[i] != s2[i])
return ((int)(s1[i] - s2[i]));
if (s1[i] == '\0' || s2[i] == '\0')
return (0);
i++;
}
return (0);
}
答えは、返り値を unsigned char でキャストしていない箇所です。
実際に、strncmp の挙動を模倣するためには以下の文字の差分を計算するときに unsigned char にキャストすることが必要不可欠です。
#include <stddef.h>
int my_strncmp(const char *s1, const char *s2, size_t n)
{
size_t i;
i = 0;
while (i < n)
{
if (s1[i] != s2[i])
- return ((int)(s1[i] - s2[i]));
+ return ((int)((unsigned char)s1[i] - (unsigned char)s2[i]));
if (s1[i] == '\0' || s2[i] == '\0')
return (0);
i++;
}
return (0);
}
strncmp とは何か
FreeBSDによると、
The strncmp() function compares not more than len characters. Because strncmp() is designed for comparing strings rather than binary data, characters that appear after a `\0' character are not compared.
簡潔に言うと、文字列s1と文字列s2を先頭の文字から一文字ずつ n 文字目まで比較し、s1 > s2 で正の値、s1 < s2 で負の値、s1 = s2で 0 を返す関数です。この大小関係は一般に文字コード順によります。
なぜ unsigned char でキャストする必要があるのか
結論、例えば '\xff' など、char の最大値 127 を超えた場合でも正しく大小を比較するためです。
例として、以下の文字列を比較することを検討します。(文字列内の各値は char 型です)
"abcdef"
"\xff\xfe\xfd"
では実際に、これらの文字を strncmp("abcdef", "\xff\xfe\xfd", 16)
として文字列を比較してみます。
このとき、想定される挙動としては以下の通りです。
各文字列の先頭の一文字目を比較します。
それぞれの文字列の先頭の文字は `'a'` と `'\xff'` です。
a は ascii コードによると 97、\xff は 255 です。
よって、大小比較は 'a' < '\xff' が成り立つため、負の値が返されます。
しかし、unsigned char にキャストしない場合、正の値 が返されます。
この結果は、'\xff' が 255 ではなく -1 と認識されており、大小比較が期待通り計算できていないことが原因です。
この差が発生している原因を探るために、 (char)'\xff'
と (unsigned char)'\xff'
を定義したときの4バイト分のメモリを確認してみます。
printf("output: %08x\n", (char)'\xff'); // output: ffffffff
printf("output: %08x\n", (unsigned char)'\xff'); // output: 000000ff
前者では符号ありの数値で -1、後者では符号なしの 255 を表していることが確認できました。
また、unsigned char のキャスト有無が '\xff'
を定義したときのメモリの値に差を生むということが明らかになりました。
まとめ
'\xff' を char または unsgiend char として扱うことによって差が生じることが確認できました。そして、本家 strncmp では unsigned char にキャストして大小比較するように処理が書かれていました。
実は man コマンドで strncmp を確認したところ、以下のような記述がありました。
The comparison is done using unsigned characters
文字の大小を比較する際には unsigned char としてくださいとのことでした。完全に見落としてましたね...
Discussion
char
はsigned char
ともunsigned char
とも異なる独立した型ですがsigned char
かunsigned char
のどちらかと同じ表現・挙動であり、どちらを選ぶかは処理系定義という言語仕様になっています。char
以外の整数型 (int
やlong int
など) はsigned
もunsigned
も付けなければ暗黙にsigned
になるのでchar
だけが変則的な扱いです。つまり
char
の表現範囲がunsigned char
と同様に 0 から 255 であるような処理系があります。 理由はよくわかりませんがインテル系ではchar
はsigned char
相当、モトローラ系ではunsigned char
相当としていることが多いようです。char
がunsigned char
相当であるような処理系ではunsigned char
へのキャストが無くても意図通りの挙動になるはずなので、もしもそのような処理系のみをサポートすると判断することがあれば必要不可欠ということはありません。もちろんどちらの選択をした処理系でもまともに動くように配慮するに越したことはないのですが。
C の言語仕様にはそういう選択の幅がある項目が多数あるので処理系の動作を見て理解を深めようとしても他の処理系では違うということがそれなりにあります。
非常にためになることをコメントしていただきありがとうございます。
今後の参考にさせていただきます。