45282


以前、以下で投稿した内容の関連質問となります。

[C言語でPOSIX規定関数のlfind関数で配列要素にマッチした文字列の参照方法](https://ja.stackoverflow.com/q/45239)

Ubuntu 16.04+gcc 5.4.0で，以下のサンプルコードでC言語のlfind関数により，文字列配列とint型の配列から指定した要素
("break"と2)を検索しています。

[code]

    /// \file example_lfind.c
    #include <stdio.h>
    #include <string.h>
    #include <search.h>
    
    int main(void) {
        // lfind for string array.
        char *tab1[] = {"auto", "break", "continue"};
        size_t size1 = sizeof(tab1)/sizeof(tab1[0]);
        char *key1 = "break";
        char *entry1 = lfind(&key1, tab1, &size1, sizeof(tab1[0]), (int (*)(const void *, const void*))strcmp);
    
        if (entry1) {
            printf("array: %p:%s\n", (void *)&tab1[1], tab1[1] );
            printf("found: %p:%s\n", entry1, entry1);
        } else {
            puts("STR NOT FOUND");
        }
    
        // lfind for int array.
        int tab2[] = {1, 2, 3};
        size_t size2 = sizeof(tab2)/sizeof(tab2[0]);
        int key2 = 2;
        int *entry2 = lfind(&key2, tab2, &size2, sizeof(tab2[0]), (int (*)(const void *, const void*))strcmp);
    
        if (entry2) {
            printf("array: %p:%d\n", (void *)&tab2[1], tab2[1] );
            printf("found: %p:%d\n", (void *)entry2, *entry2);
        } else {
            puts("INT NOT FOUND");
        }
    
        return 0;
    }
    
[/code]

実行結果例は以下となります。

[code]

    array: 0x7ffe40601408:break
    found: 0x7ffe40601408:@
    array: 0x7ffe406013f4:2
    found: 0x7ffe406013f4:2
    
[/code]

冒頭にあげた質問で，文字列配列 (tab1) でマッチした entry1
はlfindの第5引数に指定する検索に使用する比較関数に渡される引数と，結果を受け取るポインター(entry1)のデータ型が合っていないので，正しくマッチ後の値を参照できずに，"break"ではなく"@"
(NULL?)が表示されていることがわかりました。

ただ，マッチ自体はうまくできているようにみえます。int型の配列の検索結果entry2に至っては，検索した値 (2)
も検索結果のentry2からきちんと参照できています。

しかし，先の質問で以下の通り，[たまたまうまくいっているだけとの指摘](https://ja.stackoverflow.com/a/45241/29148)をいただきました。

>
> あなたのコードではポインタの内容の4または8バイトの内容をstrcmpで比較してしまっています。ポインタそのものを表すバイトの途中に0x00が現れれば、誤った結果を出すでしょうし、逆に0x00がどこにもない領域に配列が置かれていたら、メモリ未割り当ての領域アクセスで異常終了するかもしれません。

そこで，2点の質問です。

  1. 今回の比較関数にstrcmpを使ったchar* 配列のtab1とint配列のtab2の検索でうまくいかない具体的なパターンを教えてほしい。
  2. 配列の検索は頻出事項であり，可能であれば準標準関数であるlfindとlfindの第5引数の比較関数に採用可能な標準関数(strcmpなど)だけで(できれば自前での比較用関数の実装は避けたい)，手短に文字列配列，数値配列 (int, float/double)の検索を実現したい。なにかいい方法があれば教えてほしい (文字配列はstrchr単独で検索可能なため除外)。

片方だけの回答でも歓迎です。どうぞよろしくお願いします。


CC BY-SA 4.0
2018-07-03T14:30:36.303
45278
2021-03-10T06:09:43.953
2021-03-10T06:09:43.953
3060
29148
question
c posix
C言語のPOSIX定義関数のlfindで配列要素の検索がうまくできているか自信がない


  1. **_今回の比較関数にstrcmpを使ったchar_ 配列のtab1とint配列のtab2の検索でうまくいかない具体的なパターンを教えてほしい。***

まずは、こちらの後半の質問にお答えしておきます。

まずは、int用のテストコードをこちらのように修正して実行して見てください。

[code]

    // lfind for int array.
    int tab2[] = {256, 512, 768};
    size_t size2 = sizeof(tab2)/sizeof(tab2[0]);
    int key2 = 512;
    int *entry2 = lfind(&key2, tab2, &size2, sizeof(tab2[0]), (int (*)(const void *, const void*))strcmp);
    
    if (entry2) {
        printf("array: %p:%d\n", (void *)&tab2[1], tab2[1] );
        printf("found: %p:%d\n", entry2, *entry2);
    } else {
        puts("INT NOT FOUND");
    }
    
[/code]

実行結果はこんな風になるはずです。

[code]

    array: 0x7ffeefbff5e0:512
    found: 0x7ffeefbff5dc:256
    
[/code]

`lfind`による検索結果は`key2`の値に一致する2個目ではなく、1個目の256だと出力されています。これは`int`型の数値の`256`や`512`がどのようにメモリ中に保存されているのかを考えるとわかります。

(Big endianの特殊なUbuntuではないと思うので、little endianで示しています。)

[code]

    256 -> 0x00000100 -> |0x00|0x01|0x00|0x00|
    512 -> 0x00000200 -> |0x00|0x02|0x00|0x00|
    
[/code]

`strcmp`は引数のポインターから、NUL文字、つまり0x00となるまでの範囲を比較するのはご存知かと思います。したがって`256`が格納された領域の先頭アドレスと`512`が格納された先頭アドレスにはどちらも先頭に0x00、NUL文字が入っていることになるので、そこで比較が終わってしまうのです。

[code]

    int a = 256;
    int b = 512;
    int result = strcmp((char *)&a, (char *)&b);
    printf("result=%d\n", result);
    
[/code]

`result`の値は、「等しい」を表す`0`になるはずです。

(`strcmp("", "")`の結果が0になるのと同じなんですが、お分かりでしょうか。)

* * *

`char
*`の場合は、ポインターで、ポインターそのもののアドレス値が特定のビットパターンになるようにするのは難しいのですが、ポインターの実態は単に整数値で`0x7ffe40601408`だとか、`0x7ffeefbff5e0`とか言った中身になっているのは`%p`の出力でお分かりでしょう。

たまたま2つのポインターが`0x7ffe40601400`と`0x7ffeefbff500`なんてアドレスになっていたら、上記の`256`と`512`と同様に全体としては全然違うアドレスだけど、`strcmp`が等しいと判定してしまうと言うことが起こり得ます。

ここでは例を簡単にするために最下位の1バイトが0x00になるような例を作りましたが、もちろん最下位以外のどこかのバイトが0x00でそこまでは全バイト等しい、なんてアドレスの比較でも`strcmp`は「等しい」と判定してしまうでしょう。

(例えば、`0x7ffe40600014`と`0x7ffeefbf0014`を上の`a`と`b`に入れてみてください。)

ちなみにこのご質問のstring
array版では、ポインターのアドレス値同士を`strcmp`で比較してしまっているので、「アドレスは違っているけど、ポインターの指す先にある文字列は同じ内容」と言う場合にも所望の動作をしなくなります。

* * *

  2. **_配列の検索は頻出事項であり，可能であれば準標準関数であるlfindとlfindの第5引数の比較関数に採用可能な標準関数(strcmpなど)だけで(できれば自前での比較用関数の実装は避けたい)，手短に文字列配列，数値配列 (int, float/double)の検索を実現したい。なにかいい方法があれば教えてほしい (文字配列はstrchr単独で検索可能なため除外)。_**

私的には、「できれば自前での比較用関数の実装は避けたい」なんてことにこだわるよりも、比較関数の定型的な書き方をさっさと身につけた方が良いのではないか、と言うところです。

`int`型の比較を行う正しいコードはこんな感じになります。

[code]

    int cmpare_int(int *pInt1, int *pInt2) {
        int i1 = *pInt1;
        int i2 = *pInt2;
    
        if( i1 < i2 ) {
            return -1;
        } else if( i1 == i2 ) {
            return 0;
        } else {
            return 1;
        }
    }
    
    int main(void) {
    
        // lfind for int array.
        int tab2[] = {256, 512, 768};
        size_t size2 = sizeof(tab2)/sizeof(tab2[0]);
        int key2 = 512;
        int *entry2 = lfind(&key2, tab2, &size2, sizeof(tab2[0]), (int (*)(const void *, const void*))cmpare_int);
    
        if (entry2) {
            printf("array: %p:%d\n", (void *)&tab2[1], tab2[1] );
            printf("found: %p:%d\n", entry2, *entry2);
        } else {
            puts("INT NOT FOUND");
        }
    
        return 0;
    }
    
[/code]

結果は正しく、

[code]

    array: 0x7ffeefbff5e0:512
    found: 0x7ffeefbff5e0:512
    
[/code]

と言った感じになるはずです。

左の方が小さければ負の値、等しければ0、そうでなければ(左のほうが大きければ)正の値を返すと言う処理を書くだけです。ちょくちょく出てくる処理を書くために(下手すると自分で専用処理を書くのと大差ない長さの)比較関数を書かないといけないと言うのは、ある意味C言語の制約上仕方ないと言ったところでしょうか。

(ちなみに`cmpare_int`関数は`lfind`専用でよければもう少し簡略化できますが、`bsearch`等に使えなくなってしまうため、そちらのコードは掲載しないでおきます。)

この辺をしっかり身につけてからテンプレートやらジェネリクスやらのある言語をやると、その便利さがよくわかるかもしれません。


CC BY-SA 4.0
2018-07-03T15:48:14.873
45282
2018-07-03T15:48:14.873
13972
45278
answer