strncmp を実装する際に困ったこと
2024-01-15
azblob://2024/01/15/eyecatch/2024-01-15-strncmp-000.jpg

突然ですが、以下の関数は `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 にキャストすることが必要不可欠です。

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"`
    ※ C 言語で \x は 16 進数の表記であり、10 進数の数値で表すと 255 です。
    この数値は ascii コードには含まれない文字です。

では実際に、これらの文字を `strncmp("abcdef", "\xff\xfe\xfd", 16)` として文字列を比較してみます。
このとき、想定される挙動としては以下の通りです。

各文字列の先頭の一文字目を比較します。
それぞれの文字列の先頭の文字は `'a'` と `'\xff'` です。
a は ascii コードによると 97、\xff は 255 です。
よって、大小比較は 'a' < '\xff' が成り立つため、負の値が返されます。

しかし、unsigned char にキャストしない場合、正の値が返されます。

※ 本家 strncmp で想定される返り値は負の値です。

この結果は、'\xff' が 255 ではなく -1 と認識されており、大小比較が期待通り計算できていないことが原因です。
この差が発生している原因を探るために、 `(char)'\xff'` と `(unsigned char)'\xff'` を定義したときの4バイト分のメモリを確認してみます。
 

Cprintf("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 としてくださいとのことでした。完全に見落としてましたね...

tag C