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 << "знайдено на позиції " << 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() є — чудово, код читатиметься приємніше; якщо немає — використовуйте старий надійний спосіб.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ