web-dev-qa-db-ja.com

strchr実装の仕組み

Strchr()メソッドの独自の実装を作成しようとしました。

これは次のようになります。

char *mystrchr(const char *s, int c) {
    while (*s != (char) c) {
        if (!*s++) {
            return NULL;
        }
    }
    return (char *)s;
}

最後の行はもともとあった

return s;

しかし、これはsがconstであるため機能しませんでした。このキャスト(char *)が必要であることがわかりましたが、正直に私はそこで何をしているのかわかりません:(誰かが説明できますか?

18
Marc

これは実際にはC標準のstrchr()関数の定義の欠陥だと思います。 (私は間違っていることが証明されて喜んでいます。)(コメントに答えて、それが本当に欠陥かどうかは議論の余地があります。私見それはまだ貧弱なデザインです。それはできます安全に使用できますが、安全に使用するのは簡単すぎます。)

C標準の内容は次のとおりです。

char *strchr(const char *s, int c);

strchr関数は、sが指す文字列内で最初に出現するccharに変換)を見つけます。終端のnull文字は文字列の一部と見なされます。

つまり、このプログラムは:

#include <stdio.h>
#include <string.h>

int main(void) {
    const char *s = "hello";
    char *p = strchr(s, 'l');
    *p = 'L';
    return 0;
}

文字列リテラルへのポインタをconstcharへのポインタとして注意深く定義していますが、文字列リテラルを変更するため、動作は未定義です。少なくともgccはこれについて警告せず、プログラムはセグメンテーション違反で終了します。

問題は、strchr()const char*引数を取ることです。これは、sが指すデータを変更しないことを約束しますが、単純なchar*を返し、呼び出し元が同じデータ。

ここに別の例があります。 未定義の動作はありませんが、 キャストなしでconst修飾オブジェクトを静かに変更します(さらに考えれば、未定義の動作があると思います)。

#include <stdio.h>
#include <string.h>

int main(void) {
    const char s[] = "hello";
    char *p = strchr(s, 'l');
    *p = 'L';
    printf("s = \"%s\"\n", s);
    return 0;
}

つまり、(あなたの質問に答えるために)strchr()のC実装は、その結果をキャストしてconst char*からchar*に変換するか、同等のことを行う必要があると思います。

これが、C++がC標準ライブラリに対して行ういくつかの変更の1つで、strchr()を同じ名前の2つのオーバーロードされた関数に置き換える理由です。

const char * strchr ( const char * str, int character );
      char * strchr (       char * str, int character );

もちろん、Cはこれを行うことはできません。

もう1つの方法は、strchrを2つの関数に置き換えることです。1つはconst char*を取得してconst char*を返し、もう1つはchar*を取得してchar*を返します。 C++とは異なり、2つの関数はおそらくstrchrstrcchrのように異なる名前を付ける必要があります。

(これまでは、strchr()がすでに定義されていた後、constがCに追加されました。これが、既存のコードを壊すことなくstrchr()を維持する唯一の方法でした。)

strchr()は、この問題がある唯一のC標準ライブラリ関数ではありません。影響を受ける関数のリスト(Ithinkこのリストは完全ですが、保証はしません):

void *memchr(const void *s, int c, size_t n);
char *strchr(const char *s, int c);
char *strpbrk(const char *s1, const char *s2);
char *strrchr(const char *s, int c);
char *strstr(const char *s1, const char *s2);

(すべて<string.h>で宣言)および:

void *bsearch(const void *key, const void *base,
    size_t nmemb, size_t size,
    int (*compar)(const void *, const void *));

<stdlib.h>で宣言)。これらすべての関数は、配列の最初の要素を指すconstデータへのポインターを受け取り、その配列の要素への非constポインターを返します。

19
Keith Thompson

非constポインターを非変更関数からconstデータに返す方法は、実際にはC言語で広く使用されているidiomです。それはいつもきれいであるとは限りませんが、かなり確立されています。

ここでの理由は簡単です:strchr自体は変更しない操作です。ただし、定数文字列と非定数文字列の両方にstrchr機能が必要です。これにより、入力の定数が出力の定数に伝播されます。 CでもC++でも、この概念に対するエレガントなサポートは提供されていません。つまり、両方の言語で、const-correctnessによるリスクを回避するために、実質的に同一の関数twoを記述する必要があります。

C++では、同じ名前の2つの関数を宣言することで、関数のオーバーロードを使用できます。

const char *strchr(const char *s, int c);
char *strchr(char *s, int c);

Cでは関数のオーバーロードがないため、この場合にconst-correctnessを完全に適用するには、次のようなdifferentの名前を持つ2つの関数を提供する必要があります

const char *strchr_c(const char *s, int c);
char *strchr(char *s, int c);

場合によってはこれが正しいことかもしれませんが、通常(そして当然のことながら)扱いにくく、C標準に関与していると見なされています。関数を1つだけ実装することで、この状況をよりコンパクトな(ただし、よりリスクが高い)方法で解決できます。

char *strchr(const char *s, int c);

これは、const以外のポインタを入力文字列に返します(出口でキャストを使用して、正確に実行します)。このアプローチは言語の規則に違反していませんが、呼び出し元に違反する手段を提供しています。データのconstnessをキャストすることにより、このアプローチはconst-correctnessを監視する責任を関数自体から呼び出し元に委譲するだけです。呼び出し側が何が起こっているのかを認識し、「ニースを演じる」ことを覚えている限り、つまりconst修飾ポインターを使用してconstデータを指している限り、そのような関数によって作成されたconst-correctnessの壁の一時的な違反は即座に修復されます。

私はこのトリックを、(特に関数のオーバーロードがない場合に)不要なコードの重複を減らすための完全に受け入れ可能なアプローチだと考えています。標準ライブラリはそれを使用します。自分が何をしているのかを理解していれば、それを避ける理由もありません。

さて、あなたのstrchrの実装に関しては、スタイルの観点からは奇妙に見えます。サイクルヘッダーを使用して、操作している全範囲(完全な文字列)を反復処理し、内側のifを使用して早期終了条件をキャッチします

for (; *s != '\0'; ++s)
  if (*s == c)
    return (char *) s;

return NULL;

しかし、そのようなことは常に個人的な好みの問題です。誰かがちょうど好むかもしれません

for (; *s != '\0' && *s != c; ++s)
  ;

return *s == c ? (char *) s : NULL;

関数内の関数パラメーター(s)を変更することは悪い習慣だと言う人もいます。

14
AnT

constキーワードは、パラメーターを変更できないことを意味します。

sconst char *sとして宣言され、関数の戻り値の型はchar *であるため、sを直接返すことはできませんでした。コンパイラーがそれを許可した場合、const制限をオーバーライドすることが可能です。

char*に明示的なキャストを追加すると、コンパイラーに何をしているのかがわかります(ただし、Ericが説明したように、実行しなかった方がよいでしょう)。

更新:コンテキストの都合上、エリックの答えを引用します。彼はそれを削除したようです。

Sはconst char *であるため、変更しないでください。

代わりに、char *型の結果を表すローカル変数を定義し、メソッド本体のsの代わりにそれを使用します。

1
Alberto Miranda

関数の戻り値は、文字への定数ポインタでなければなりません:

strchrconst char*を受け入れ、const char*も返す必要があります。戻り値が入力文字配列を指すため、潜在的に危険な非定数を返しています(呼び出し元は定数引数が一定のままであることを期待している可能性がありますが、その一部がchar *ポインタ)。

一致する文字が見つからない場合、関数の戻り値はNULLになります。

また、strchrは、目的の文字が見つからない場合にNULLを返すことになっています。文字が見つからないときにNULL以外、またはこの場合はsを返す場合、呼び出し元(動作がstrchrと同じであると考える場合)は、結果の最初の文字が実際に一致する(NULL戻り値なし)と想定する場合があります。一致したかどうかを判断する方法はありません)。

(それがあなたが意図したことかどうかはわかりません。)

これを行う関数の例を次に示します。

この関数についていくつかのテストを作成して実行しました。潜在的なクラッシュを回避するために、いくつかの本当に明らかな健全性チェックを追加しました。

const char *mystrchr1(const char *s, int c) {
    if (s == NULL) {
        return NULL;
    }
    if ((c > 255) || (c < 0)) {
        return NULL;
    }
    int s_len;
    int i;
    s_len = strlen(s);
    for (i = 0; i < s_len; i++) {
        if ((char) c == s[i]) {
            return (const char*) &s[i];
        }
    }
    return NULL;
}
0
A B