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":
| Что ищем | Пример результата | Почему |
|---|---|---|
|
|
слово начинается с индекса |
|
|
такого фрагмента нет |
Мини-пример: ищем слово
#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() есть — отлично, код будет читаться приятнее; если нет — используем старый надёжный способ.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ