JavaRush /Курсы /C++ SELF /Методы std::string: find, substr, starts_with, ends_with,...

Методы std::string: find, substr, starts_with, ends_with, contains

C++ SELF
9 уровень , 1 лекция
Открыта

1. Введение

Если смотреть на строки как на «массив символов», то возникает соблазн делать всё вручную: искать @ в email циклом, вырезать домен посимвольно, проверять расширение файла через сравнение последних четырёх символов… Это возможно, но быстро превращает код в хаос.

В стандартной библиотеке C++ есть набор методов, которые делают типовые текстовые операции гораздо проще и читаемее. Это как раз тот случай, когда «короче» обычно значит ещё и «понятнее». Сегодня мы освоим четыре идеи: поиск (find()), извлечение (substr()), проверка префикса/суффикса (starts_with(), ends_with()) и проверка «содержит ли» (contains() или её эквивалент через find()).

2. Поиск в строке: find() и проверка «содержит ли»

Когда вы нажимаете Ctrl+F в редакторе и вводите слово, редактор возвращает вам позицию найденного фрагмента. В std::string примерно та же модель: метод find(...) пытается найти первое вхождение символа или подстроки и возвращает позицию (индекс), с которой найдено совпадение.

Важно привыкнуть к мысли: find() не возвращает true/false. Он возвращает позицию, а если не найдено — возвращает специальное значение.

Что возвращает find()

find() возвращает тип std::size_t — тот же тип, что и size(). Это логично: позиция в строке — это индекс, а индексы и размеры в стандартной библиотеке обычно живут в std::size_t.

Если совпадение не найдено, find() возвращает std::string::npos. Это не «-1 в красивой обёртке», это отдельное специальное значение (по сути очень большое число типа size_t). Поэтому проверять нужно строго так: pos != std::string::npos.

Давайте зафиксируем на маленькой таблице. Возьмём строку text = "one two three":

Что ищем Пример результата Почему
"two"
4
слово
two
начинается с индекса
4
"cat"
std::string::npos
такого фрагмента нет

Мини-пример: ищем слово

#include <iostream>
#include <string>

int main() {
    std::string text = "one two three";
    std::size_t pos = text.find("two");

    if (pos != std::string::npos) {
        std::cout << "found at " << pos << '\n'; // found at 4
    } else {
        std::cout << "not found\n";
    }
}

Здесь прям видно контракт: либо «нашёл и даю позицию», либо «не нашёл и возвращаю npos».

find() и поиск символа

Часто нам нужен не кусок текста, а один символ-разделитель: например @ в email или . в имени файла.

#include <iostream>
#include <string>

int main() {
    std::string email = "alice@example.com";
    std::size_t at = email.find('@');

    if (at != std::string::npos) {
        std::cout << "@ at " << at << '\n'; // @ at 5
    } else {
        std::cout << "no @\n";
    }
}

Самое важное правило безопасности для find()

Перед тем как использовать позицию, всегда проверяйте pos != std::string::npos.

Почему это настолько критично? Потому что npos нельзя использовать как индекс. А ещё опаснее — делать pos + 1, если pos == npos: получится огромное число, и дальше вы либо вылетите в ошибку выполнения, либо получите мусор (в зависимости от операции).

«Строка содержит…»: через contains() или через find()

Когда программист говорит «проверим, содержит ли строка подстроку», он почти никогда не хочет знать позицию. Ему нужен ответ уровня да/нет. В более новых стандартах C++ у std::string появился метод contains(), который делает это напрямую.

При этом важно понимать: даже если contains() недоступен в вашем компиляторе/настроенном стандарте, логика легко выражается через find().

Проверка через find() (работает везде)

#include <iostream>
#include <string>

int main() {
    std::string filename = "report_2026.txt";

    bool has_year = filename.find("2026") != std::string::npos;
    std::cout << has_year << '\n'; // 1
}

Если contains() доступен

В C++23 std::string::contains() обычно уже есть (зависит от реализации стандартной библиотеки). Он делает то же самое, но читается приятнее: «строка содержит это».

#include <iostream>
#include <string>

int main() {
    std::string filename = "report_2026.txt";

    // Если ваша стандартная библиотека поддерживает C++23:
    std::cout << filename.contains("2026") << '\n'; // 1
}

Если у вас этот код вдруг не компилируется — это не «вы сломали C++». Это означает, что конкретная библиотека/компилятор ещё не довёз поддержку contains() или проект собран не в режиме C++23. Тогда спокойно используйте find() != npos.

3. Извлечение подстроки: substr()

Теперь логичный вопрос: хорошо, мы нашли позицию. А как получить часть строки? Например, домен из email или имя файла без расширения.

Для этого есть substr().

У substr() два основных варианта:

  • s.substr(pos) — взять всё от pos до конца;
  • s.substr(pos, count) — взять count символов, начиная с pos.

Ключевой момент: второй параметр — это длина, а не «индекс конца». Новички очень часто путают это, потому что мозг рисует «диапазон от pos до count», но библиотека думает иначе: «начни в pos и возьми count символов».

Схема: индексы и substr()

Возьмём строку:

email = "alice@example.com"
         0123456789...

Если at = 5 (позиция @), то домен начинается с at + 1, то есть с 6:

email.substr(6) "example.com"

А теперь пример, где нужен второй параметр. Хотим имя до @:

email.substr(0, at) — взять at символов начиная с 0 "alice"

Мини-пример: вырезаем домен из email

#include <iostream>
#include <string>

int main() {
    std::string email = "alice@example.com";
    std::size_t at = email.find('@');		// "at" - это "собака"

    if (at != std::string::npos) {
        std::string name = email.substr(0, at);		
        std::string domain = email.substr(at + 1);	// пропускаем собаку

        std::cout << name << '\n';   // alice
        std::cout << domain << '\n'; // example.com
    }
}

Аккуратность с границами

Если pos больше, чем s.size(), то substr(pos, ...) приведёт к ошибке выполнения (обычно это исключение std::out_of_range, но мы пока не разбираем обработку исключений — просто держим в голове: так делать нельзя).

Поэтому нормальный стиль такой: pos почти всегда берётся из find(), и мы заранее проверяем pos != npos. Это не «лишняя проверка», это страховка от ночных кошмаров.

4. Проверки начала и конца: starts_with() и ends_with()

Поиск — это хорошо, но иногда нам не нужно искать «где угодно». Иногда формат строки предполагает чёткую структуру: «должно начинаться с "http://"», «должно заканчиваться на ".txt"», «команда должна начинаться с "add "».

Для таких случаев существуют starts_with() и ends_with(). Эти методы были добавлены в стандартную библиотеку как отдельная улучшенная поддержка строковых проверок.

Почему это удобнее, чем find()? Потому что читаемость: starts_with("cmd:") выглядит как проверка формата, а не как «поиск чего-то непонятно где».

Мини-пример: проверяем расширение и префикс

#include <iostream>
#include <string>

int main() {
    std::string filename = "report_2026.txt";

    std::cout << filename.starts_with("report_") << '\n'; // 1
    std::cout << filename.ends_with(".txt") << '\n';      // 1
}

Можно ли сделать то же самое через find()

Да, можно:

  • starts_with(prefix) эквивалентно проверке s.find(prefix) == 0 (но нужно быть осторожным: find() вернёт npos, если не найдено);
  • ends_with(suffix) вручную через индексы тоже делается, но получается чуть длиннее.

И вот здесь важно ощущение: мы не просто экономим пару символов кода. Мы делаем проверку более явной. Если вы через месяц откроете этот код, ends_with(".txt") будет читаться без расшифровки.

5. Мини-проект: “TextInspector”

Чтобы всё это не осталось «на уровне отдельных фрагментов», соберём маленькое консольное приложение, которое читает одну строку и делает несколько проверок. Мы не строим полноценный парсер команд (это будет позже, когда появится токенизация), но уже можем аккуратно извлечь смысл из текста.

Идея простая: пользователь вводит строку, а программа пытается понять, что это похоже на:

  • email (если содержит @);
  • имя файла (если заканчивается на ".txt");
  • URL-подобную строку (если начинается с "http://" или "https://").

Каркас: читаем строку и печатаем базовые ответы

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    std::cout << "len=" << s.size() << '\n'; // пример: len=17
    std::cout << "has @: " << (s.find('@') != std::string::npos) << '\n';
}

Выделяем домен, если это похоже на email

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    std::size_t at = s.find('@');
    if (at != std::string::npos && at + 1 < s.size()) {
        std::string domain = s.substr(at + 1);
        std::cout << "domain=[" << domain << "]\n"; // domain=[example.com]
    } else {
        std::cout << "not an email\n";
    }
}

Здесь мы добавили важную микропроверку at + 1 < s.size(). Она защищает от строки "alice@", где @ есть, но домена после него нет.

Проверяем расширение файла через ends_with()

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    if (s.ends_with(".txt")) {
        std::cout << "looks like a text file\n"; // looks like a text file
    } else {
        std::cout << "not .txt\n";
    }
}

Проверяем URL-подобность через starts_with()

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    bool http = s.starts_with("http://") || s.starts_with("https://");
    std::cout << "url-like: " << http << '\n'; // url-like: 1
}

Собираем всё вместе

Код всё ещё короткий, но уже похож на приложение, а не на набор экспериментов:

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    std::cout << "len=" << s.size() << '\n';

    if (s.starts_with("http://") || s.starts_with("https://")) {
        std::cout << "type=url\n";
    } else if (s.find('@') != std::string::npos) {
        std::size_t at = s.find('@');
        std::cout << "type=email, domain=" << s.substr(at + 1) << '\n';
    } else if (s.ends_with(".txt")) {
        std::cout << "type=text-file\n";
    } else {
        std::cout << "type=unknown\n";
    }
}

Обратите внимание на хороший «читаемый» порядок проверок: сначала начало строки (это быстро и ясно), потом наличие маркера @, потом окончание ".txt". В реальных задачах порядок условий — это часть дизайна: вы решаете, какие форматы важнее и как разрешать неоднозначности.

6. Типичные ошибки при работе с find(), substr(), starts_with(), ends_with(), contains()

Ошибка №1: сравнивать результат find() с -1.
Так иногда делают по привычке из других языков или из старых примеров. В C++ find() возвращает std::size_t, и «не найдено» обозначается std::string::npos. Сравнение с -1 выглядит как «почти работает», но на самом деле это неправильный контракт, который легко ломается при чтении и переносе кода.

Ошибка №2: использовать позицию из find(), не проверяя npos.
Сначала кажется, что «ну я же уверен, что символ там есть». Потом приходит пользователь и вводит строку без @, а программа пытается сделать substr(npos + 1) и улетает в ошибку выполнения. Если позиция пришла из find(), нормальный путь один: сначала if (pos != std::string::npos), и только потом любая арифметика и любые substr().

Ошибка №3: путать второй параметр substr(pos, count) с «конечным индексом».
Это одна из самых частых ловушек. В substr() второй аргумент — длина, то есть «сколько символов взять», а не «до какого индекса». Поэтому конструкции вроде substr(begin, end) почти всегда ошибочны: правильно substr(begin, end - begin), если end — позиция после конца диапазона.

Ошибка №4: забывать, что starts_with()/ends_with() — это про структуру, а не про поиск.
Иногда новички пытаются заменить все проверки на starts_with(), а потом удивляются, что "abcXdef" не starts_with("X"). Это нормально: starts_with() — проверка префикса, а «содержит ли» — это либо contains(), либо find() != npos. Когда вы выбираете метод, мысленно проговаривайте задачу: «в начале? в конце? где угодно?».

Ошибка №5: считать contains() обязательным и «ломаться», если его нет.
Даже в режиме C++23 иногда встречаются окружения, где стандартная библиотека обновлена не полностью. Это не повод переписывать проект. Проверка «содержит ли» всегда выражается через find(...) != std::string::npos. Если contains() есть — отлично, код будет читаться приятнее; если нет — используем старый надёжный способ.

1
Задача
C++ SELF, 9 уровень, 1 лекция
Недоступна
Поиск фразы
Поиск фразы
1
Задача
C++ SELF, 9 уровень, 1 лекция
Недоступна
Проверка шаблона
Проверка шаблона
1
Задача
C++ SELF, 9 уровень, 1 лекция
Недоступна
Разбор адреса
Разбор адреса
1
Задача
C++ SELF, 9 уровень, 1 лекция
Недоступна
Логи и теги
Логи и теги
Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #2923722 Уровень 17
14 мая 2026
/ru/javarush/cpp/core/level09/task04/solution.cpp: In function ‘int main()’: /ru/javarush/cpp/core/level09/task04/solution.cpp:13:19: error: ‘std::string’ {aka ‘class std::__cxx11::basic_string<char>’} has no member named ‘starts_with’ 13 | if(logMessage.starts_with("ERROR: ")) level = "ERROR"; | ^~~~~~~~~~~ /ru/javarush/cpp/core/level09/task04/solution.cpp:14:24: error: ‘std::string’ {aka ‘class std::__cxx11::basic_string<char>’} has no member named ‘starts_with’ 14 | else if(logMessage.starts_with("WARN: ")) level = "WARN"; | ^~~~~~~~~~~ Сколько ещё задач не будут проходить решения ? Косячники )))
Андрей Асеев Уровень 12
13 апреля 2026
В третей задаче, неявный запрет на использование starts_with и ends_with, вообще можно, кончено догадаться по условию, но совсем не явно, 5-й пункт требований
Андрей Асеев Уровень 12
13 апреля 2026
Вторая задача выдает ошибку при запуске в web IDE ‘std::string’ {aka ‘class std::__cxx11::basic_string<char>’} has no member named ‘starts_with’ Но проверку проходит.