Моя часть проекта: функции поиска по строке, токенизация, strerror и спецфункции
из Part 5. Ниже по каждой функции: что она делает, объявление, как устроена
реализация и пара примеров.
Везде, где речь о нулевом указателе, используется макрос проекта S21_NULL (он
определён в s21_string.h как ((void *)0)). Стандартный NULL в коде не
используется по условию задачи.
Код разложен по трём файлам, чтобы он не пересекался с чужими и проще мержился:
s21_string_search.c — поиск и сравнение: strchr, strrchr, strpbrk, strcspn, strncmp, strstr, strtok;s21_strerror.c — s21_strerror и таблицы ошибок для mac/linux;s21_special.c — Part 5: to_upper, to_lower, insert, trim.Везде соблюдены правила структурного программирования, принятые в проекте:
return в конце функции; ранний выход разрешаю себе только при проверкеS21_NULL);break. Вместо break завожу флаг (done, stop,found) и добавляю его в условие цикла;goto, без глобальных переменных;clang-format (Google style) и собирается с-Wall -Werror -Wextra -std=c11 без предупреждений.Три функции (strpbrk, strcspn, strtok) задают один и тот же вопрос: входит ли
символ в набор? Чтобы не повторять цикл, я вынес это в статическую функцию:
static int s21_is_delim(char c, const char *delim); // 1 — да, 0 — нет
В Part 5 лежит её аналог s21_in_set. Я сознательно не стал делать один общий
помощник на два файла, чтобы файлы оставались независимыми друг от друга.
Ищет первое вхождение символа c в строке str, возвращает указатель на него или
S21_NULL.
char *s21_strchr(const char *str, int c);
c привожу к char и иду по строке слева направо. На первом совпадении ставлю
флаг done, и цикл завершается по условию. Отдельно обрабатываю случай c == '\0':
по стандарту функция должна вернуть указатель на завершающий нуль, поэтому после
цикла проверяю это.
Примеры:
| Вызов | Результат |
|---|---|
s21_strchr("hello", 'l') |
указатель на первую l |
s21_strchr("hello", 'z') |
S21_NULL |
s21_strchr("hi", '\0') |
указатель на '\0' |
То же, что strchr, только ищет последнее вхождение символа.
char *s21_strrchr(const char *str, int c);
Иду по строке до конца и при каждом совпадении перезаписываю результат, поэтому в
итоге остаётся указатель на последнее вхождение. Случай c == '\0' тоже учтён:
возвращается указатель на завершающий нуль.
Примеры:
| Вызов | Результат |
|---|---|
s21_strrchr("hello", 'l') |
указатель на вторую l |
s21_strrchr("hello", 'z') |
S21_NULL |
Возвращает указатель на первый символ строки str1, который встречается в наборе
str2. Если такого нет, возвращает S21_NULL.
char *s21_strpbrk(const char *str1, const char *str2);
Иду по str1 и для каждого символа спрашиваю s21_is_delim, есть ли он в str2.
Выход из цикла по условию result == S21_NULL: как только нашёл, цикл
останавливается сам.
Примеры:
| Вызов | Результат |
|---|---|
s21_strpbrk("hello", "ol") |
указатель на l (индекс 2) |
s21_strpbrk("hello", "xyz") |
S21_NULL |
Возвращает длину начального участка строки str1, в котором нет ни одного символа
из набора str2.
size_t s21_strcspn(const char *str1, const char *str2);
Та же логика, что у strpbrk, но ответ другой. Иду по str1, для каждого символа
проверяю s21_is_delim. Пока символ не из набора, увеличиваю счётчик. На первом
символе из набора ставлю флаг stop и возвращаю счётчик. По сути strpbrk отвечает
«где», а strcspn — «сколько символов до этого места».
Примеры:
| Вызов | Результат |
|---|---|
s21_strcspn("hello", "l") |
2 |
s21_strcspn("hello", "xyz") |
5 |
s21_strcspn("hello", "") |
5 (набор пустой) |
Сравнивает не более n байт двух строк. Возвращает 0 при равенстве,
отрицательное число если str1 меньше, положительное если больше.
int s21_strncmp(const char *str1, const char *str2, size_t n);
Иду по строкам, пока символы равны. На первом различии записываю разницу и
останавливаюсь по флагу. Два момента, без которых результат будет неправильным:
символы привожу к unsigned char перед вычитанием (иначе на байтах со старшим
битом знак уедет не туда, стандартная strncmp сравнивает именно беззнаково); и
если дошли до общего '\0' раньше n, дальше идти нельзя, строки уже кончились.
В тестах сверяю не точное число, а знак результата: по стандарту важен только он.
Примеры:
| Вызов | Результат |
|---|---|
s21_strncmp("abc", "abc", 3) |
0 |
s21_strncmp("abc", "abd", 3) |
меньше нуля |
s21_strncmp("abc", "abz", 2) |
0 (различие за пределами n) |
Ищет первое вхождение подстроки needle в строке haystack. Возвращает указатель
на начало вхождения или S21_NULL.
char *s21_strstr(const char *haystack, const char *needle);
Реализация наивная: для каждой позиции в haystack внутренним циклом посимвольно
прикладываю needle. Если needle совпал целиком, это и есть ответ. Более хитрые
алгоритмы (Бойера–Мура и подобные) для этой задачи не нужны, наивный вариант
читается понятнее. Отдельно обрабатываю пустой needle: по стандарту в этом случае
возвращается сам haystack.
Примеры:
| Вызов | Результат |
|---|---|
s21_strstr("hello", "ll") |
указатель на ll |
s21_strstr("hello", "xy") |
S21_NULL |
s21_strstr("hello", "") |
указатель на hello |
Разбивает строку на токены по любому из символов-разделителей из delim. При первом
вызове передаётся строка, при последующих — S21_NULL, и функция продолжает с того
места, где остановилась.
char *s21_strtok(char *str, const char *delim);
Состояние между вызовами хранится в static char *saved внутри функции. Это
единственная статическая переменная в моём коде: без неё strtok невозможна, такова
её семантика, и static внутри функции не является глобальной переменной.
Логика по шагам:
S21_NULL;'\0' и запомнить позицию для следующего вызова.Две детали, о которых стоит помнить. Функция изменяет исходную строку (вставляет в
неё нули), поэтому ей нельзя передавать строковый литерал, только модифицируемый
буфер. И несколько разделителей подряд считаются за один, пустых токенов между ними
не возникает.
Пример для " one,two,,three " с разделителями " ,":
| Вызов | Возврат |
|---|---|
s21_strtok(buf, " ,") |
"one" |
s21_strtok(S21_NULL, " ,") |
"two" |
s21_strtok(S21_NULL, " ,") |
"three" |
s21_strtok(S21_NULL, " ,") |
S21_NULL |
Возвращает текстовое описание кода ошибки errnum.
const char *s21_strerror(int errnum);
Главная сложность в том, что тексты ошибок различаются на macOS и Linux, а брать их
из системных sys_errlist запрещено условием. Поэтому таблицы свои: обе лежат в
s21_string.h как макрос S21_ERRORS и выбираются препроцессором по
#ifdef __APPLE__. Там же заданы размер таблицы и префикс для неизвестной ошибки:
#ifdef __APPLE__
#define S21_ERR_COUNT 108
#define S21_UNKNOWN_PREFIX "Unknown error: "
#define S21_ERRORS { ... } // 108 сообщений macOS
#else
#define S21_ERR_COUNT 134
#define S21_UNKNOWN_PREFIX "Unknown error "
#define S21_ERRORS { ... } // 134 сообщения glibc/Linux
#endif
Сама функция простая. Если 0 <= errnum < S21_ERR_COUNT, возвращаю строку из
таблицы. Иначе собираю строку вида "Unknown error: N". Число в текст перевожу
вручную маленькой функцией s21_int_to_str, причём через long, чтобы корректно
отработал INT_MIN (при простом -INT_MIN было бы переполнение).
Таблицы я не набивал по памяти, а снял с реальной strerror() на маке и на
Linux-машине, после чего тестом прогнал весь диапазон кодов и сверил посимвольно.
Расхождений нет ни на одной из ОС. Различия, которые учтены в таблицах:
| macOS | Linux (glibc) | |
|---|---|---|
| Количество сообщений | 108 (0…107) | 134 (0…133) |
Код 0 |
"Undefined error: 0" |
"Success" |
| Неизвестная ошибка | "Unknown error: N" (с двоеточием) |
"Unknown error N" (без) |
| Пропуски в нумерации | нет | есть: коды 41 и 58 |
Возвращает новую строку, копию str, переведённую в верхний регистр. При ошибке
возвращает S21_NULL.
char *s21_to_upper(const char *str);
Если str == S21_NULL, возвращаю S21_NULL. Иначе выделяю malloc(strlen + 1) и
копирую строку, попутно поднимая регистр латинских букв (a..z сдвигаю на -32),
остальные символы переношу как есть. Если malloc не удался, тоже возвращаю
S21_NULL.
Примеры:
| Вызов | Результат |
|---|---|
s21_to_upper("Hello 1") |
"HELLO 1" |
s21_to_upper(S21_NULL) |
S21_NULL |
То же, что to_upper, но в нижний регистр.
char *s21_to_lower(const char *str);
Реализация зеркальная: латинские буквы A..Z сдвигаю на +32, остальное копирую без
изменений. Память и проверки те же.
Примеры:
| Вызов | Результат |
|---|---|
s21_to_lower("Hello 1") |
"hello 1" |
s21_to_lower(S21_NULL) |
S21_NULL |
Возвращает новую строку, в которой строка str вставлена в src начиная с индекса
start_index. При ошибке возвращает S21_NULL.
char *s21_insert(const char *src, const char *str, size_t start_index);
Сначала проверки: если src или str равны S21_NULL, либо start_index больше
длины src, возвращаю S21_NULL (вставлять некуда). Иначе выделяю память под
strlen(src) + strlen(str) символов и собираю результат из трёх кусков: начало
src до индекса, затем str, затем хвост src.
Примеры:
| Вызов | Результат |
|---|---|
s21_insert("abcf", "de", 3) |
"abcdef" |
s21_insert("abc", "XX", 0) |
"XXabc" |
s21_insert("abc", "XX", 4) |
S21_NULL (индекс за пределами) |
s21_insert(S21_NULL, "XX", 0) |
S21_NULL |
Возвращает новую строку, у которой с начала и с конца убраны символы из набора
trim_chars. При ошибке возвращает S21_NULL.
char *s21_trim(const char *src, const char *trim_chars);
Если src == S21_NULL, возвращаю S21_NULL. Дальше веду два указателя навстречу:
start двигаю вправо, пока на нём символ из набора, end влево, и в конце копирую
середину. Один случай выбрал на своё усмотрение: если trim_chars == S21_NULL, я
срезаю пробельные символы по умолчанию (" \t\n\r\f\v"). Мне это показалось
полезнее, чем падать или возвращать копию без изменений. Этот момент стоит вынести
на обсуждение команды.
Примеры:
| Вызов | Результат |
|---|---|
s21_trim("***hello***", "*") |
"hello" |
s21_trim(" spaced ", S21_NULL) |
"spaced" |
s21_trim("xxxx", "x") |
"" |
s21_trim(S21_NULL, "x") |
S21_NULL |
Тесты на Check, файл tests/test_s21_string.c, запуск через make test.
string.h;s21_strerror сверяю со стандартным strerror по всему диапазону кодов, включаяstring.h нет), поэтому проверяю ожидаемыйleaks на macOS показывает ноль;s21_string_search.c и s21_special.c — 100%,s21_strerror.c около 92%. Требование «каждая функция не ниже 80%» выполнено.