Как работает реализация strchr

Я попытался написать собственную реализацию метода strchr().

Теперь он выглядит следующим образом:

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

Последняя строка изначально была

return s;

Но это не сработало, потому что s is const. Я узнал, что должен быть этот актерский состав (char *), но я честно не знаю, что я там делаю:( Может кто-нибудь объяснить?

Ответы

Ответ 1

Я считаю, что это на самом деле недостаток в стандартном определении C функции strchr(). (Я буду счастлив, если окажется ошибочным.) (Отвечая на комментарии, можно утверждать, что это действительно недостаток, ИМХО это еще плохой дизайн. Его можно использовать безопасно, но он слишком прост в использовании. p >

Вот что говорит стандарт C:

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

Функция strchr обнаруживает первое вхождение c(преобразован в char) в строке, на которую указывает s. завершающий нулевой символ считается частью строки.

Это означает, что эта программа:

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

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

хотя он тщательно определяет указатель на строковый литерал как указатель на const char, имеет поведение undefined, так как он изменяет строковый литерал. gcc, по крайней мере, не предупреждает об этом, и программа умирает с ошибкой сегментации.

Проблема заключается в том, что strchr() принимает аргумент const char*, что означает, что он promises не изменяет данные, на которые указывает s, но возвращает равную char*, что позволяет вызывающему изменить те же данные.

Вот еще один пример; он не имеет поведения undefined, но он спокойно модифицирует const квалифицированный объект без каких-либо бросков (что, по-моему, по-моему, имеет поведение undefined):

#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;
}

Я думаю, что (чтобы ответить на ваш вопрос), что реализация C strchr() должна привести свой результат, чтобы преобразовать его из const char* в char* или сделать что-то эквивалентное.

Вот почему С++ в одном из немногих изменений, внесенных в стандартную библиотеку C, заменяет strchr() двумя перегруженными функциями с тем же именем:

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

Конечно, C не может этого сделать.

Альтернативой было бы заменить strchr на две функции: один взял const char* и вернул a const char*, а другой взял a char* и вернул a char*. В отличие от С++, две функции должны иметь разные имена, возможно, strchr и strcchr.

(Исторически, const был добавлен в C после того, как strchr() уже был определен. Вероятно, это был единственный способ сохранить strchr() без нарушения существующего кода.)

strchr() не является единственной библиотечной функцией C, которая имеет эту проблему. Список затронутой функции (я думаю, что этот список завершен, но я не гарантирую его):

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 к элементу этого массива.

Ответ 2

Практика возвращения неконстантных указателей на константные данные из немодифицирующих функций на самом деле является идиомой, довольно широко используемой на языке C. Это не всегда красиво, но это довольно хорошо.

Обоснование здесь просто: strchr само по себе является операцией без модификации. Тем не менее нам нужна функциональность strchr как для постоянных строк, так и для непостоянных строк, которые также распространяли бы постоянство входа на константу вывода. Ни C, ни С++ не предоставляют какой-либо изящной поддержки этой концепции, а это означает, что на обоих языках вам придется написать две практически идентичные функции, чтобы избежать риска с const-correctness.

В С++ вы можете использовать перегрузку функций, объявив две функции с тем же именем

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

В C у вас нет функции перегрузки, поэтому, чтобы в полной мере обеспечить соответствие const-const, в этом случае вам нужно будет предоставить две функции с разными именами, что-то вроде

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

Хотя в некоторых случаях это может быть правильным, оно обычно (и по праву) считается слишком громоздким и связано со стандартами С. Вы можете разрешить эту ситуацию более компактным (хотя и более рискованным) способом, реализовав только одну функцию

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

который возвращает указатель non-const во входную строку (используя приведение на выходе, точно так же, как вы это сделали). Обратите внимание, что этот подход не нарушает никаких правил языка, хотя он предоставляет вызывающему лицу средства для их нарушения. Отбрасывая консистенцию данных, этот подход просто делегирует ответственность за соблюдение const-правильности от самой функции до вызывающего. Пока вызывающий абонент осознает, что происходит, и помнит, что "играет хорошо", то есть использует указатель со знаком 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) внутри функции является плохой практикой.

Ответ 3

Ключевое слово const означает, что параметр не может быть изменен.

Вы не можете вернуть s напрямую, потому что s объявлен как const char *s, а тип возвращаемого значения - char *. Если компилятор разрешил вам это сделать, можно было бы переопределить ограничение const.

Добавление явного приведения в char* сообщает компилятору, что вы знаете, что делаете (хотя, как объяснил Эрик, было бы лучше, если бы вы этого не сделали).

ОБНОВЛЕНИЕ: ради контекста я цитирую ответ Эрика, поскольку он, кажется, удалил его:

Вы не должны изменять s, так как это const char *.

Вместо этого определите локальную переменную, представляющую результат типа char *, и используйте это вместо s в теле метода.

Ответ 4

Возвращаемое значение функции должно быть константным указателем на символ:

strchr принимает const 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;
}