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 << "знайдено на позиції " << pos << '\n'; // знайдено на позиції 4
} else {
std::cout << "не знайдено\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 << '\n'; // @ на позиції 5
} else {
std::cout << "немає @\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 << "є @: " << (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 << "]\n"; // домен=[example.com]
} else {
std::cout << "це не 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 << "схоже на текстовий файл\n"; // схоже на текстовий файл
} else {
std::cout << "не .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: " << http << '\n'; // схоже на URL: 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 << "тип=url\n";
} else if (s.find('@') != std::string::npos) {
std::size_t at = s.find('@');
std::cout << "тип=email, домен=" << s.substr(at + 1) << '\n';
} else if (s.ends_with(".txt")) {
std::cout << "тип=text-file\n";
} else {
std::cout << "тип=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() є — чудово, код читатиметься приємніше; якщо немає — використовуйте старий надійний спосіб.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ