🐝

strncmp を実装する際に困ったこと

2024/01/15に公開2

突然ですが、以下の関数は strncmp と同様の挙動をするように実装した関数です。
ただし、明確な誤りがあります。
その誤りはどこでしょうか?

my_strncmp.c
#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 にキャストすることが必要不可欠です。

my_strncmp.c
 #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 の最大値 127

charsigned char とも unsigned char とも異なる独立した型ですが signed charunsigned char のどちらかと同じ表現・挙動であり、どちらを選ぶかは処理系定義という言語仕様になっています。

char 以外の整数型 (intlong int など) は signedunsigned も付けなければ暗黙に signed になるので char だけが変則的な扱いです。

つまり char の表現範囲が unsigned char と同様に 0 から 255 であるような処理系があります。 理由はよくわかりませんがインテル系では charsigned char 相当、モトローラ系では unsigned char 相当としていることが多いようです。

文字の差分を計算するときに unsigned char にキャストすることが必要不可欠

charunsigned char 相当であるような処理系では unsigned char へのキャストが無くても意図通りの挙動になるはずなので、もしもそのような処理系のみをサポートすると判断することがあれば必要不可欠ということはありません。

もちろんどちらの選択をした処理系でもまともに動くように配慮するに越したことはないのですが。

C の言語仕様にはそういう選択の幅がある項目が多数あるので処理系の動作を見て理解を深めようとしても他の処理系では違うということがそれなりにあります。

nakashi94nakashi94

非常にためになることをコメントしていただきありがとうございます。
今後の参考にさせていただきます。