C++

Пошук у std::string: find, npos та практичні патерни

Методи пошуку в std::string: find(), rfind(), find_first_of(), find_last_of(), find_first_not_of(), find_last_not_of(). Поняття std::string::npos. Практичні патерни: знайти всі входження, split, trim, парсинг key=value.

Пошук у std::string

Три задачі, одна тема

Погляньте на три типові запити:

FindPuzzle.cpp
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string email   = "user@example.com";
    string csv     = "Alice,30,Kyiv";
    string config  = "  timeout = 30  ";

    // 1. Чи є символ '@' у рядку?
    bool hasAt = email.find('@') != string::npos;

    // 2. Де перша кома?
    size_t comma = csv.find(',');

    // 3. Де починається перший «справжній» символ?
    size_t start = config.find_first_not_of(" \t");

    cout << "hasAt:  " << boolalpha << hasAt  << "\n"; // true
    cout << "comma:  " << comma                    << "\n"; // 5
    cout << "start:  " << start                    << "\n"; // 2

    return 0;
}
./FindPuzzle
$ ./SearchPuzzle
hasAt: true
comma: 5
start: 2
Execution finished with exit code 0.

Три методи — три різних питання про рядок. У цій статті ми розберемо весь арсенал методів пошуку std::string та збудуємо з них корисні функції.


std::string::npos — «не знайдено»

Перш ніж розглядати методи пошуку, необхідно зрозуміти значення, яке вони повертають при невдачі.

Усі методи пошуку std::string повертають std::string::size_type (що є псевдонімом size_t — беззнакового цілого). Коли елемент не знайдено, вони повертають спеціальну константу:

static constexpr size_type npos = static_cast<size_type>(-1);

Оскільки size_type — беззнаковий тип, -1 перетворюється на SIZE_MAX — найбільше можливе значення (зазвичай 18446744073709551615 на 64-бітних системах). Це число значно більше за будь-який реальний розмір рядка, тому воно є безпечним «сигнальним» значенням.

NposExample.cpp
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string s = "Hello, World!";

    size_t pos1 = s.find("World");
    size_t pos2 = s.find("xyz");

    cout << "npos = " << string::npos << "\n";

    if (pos1 != string::npos)
        cout << "\"World\" знайдено на позиції " << pos1 << "\n";

    if (pos2 == string::npos)
        cout << "\"xyz\" не знайдено\n";

    return 0;
}
./NposExample
$ ./NposDemo
npos = 18446744073709551615
"World" знайдено на позиції 7
"xyz" не знайдено
Execution finished with exit code 0.
Ніколи не перевіряйте результат пошуку через pos >= 0 чи pos != -1size_t беззнаковий, від'ємних значень не існує. Завжди порівнюйте з std::string::npos: if (pos != std::string::npos).

Методи find та rfind

.find() — пошук першого входження

.find() шукає підрядок або символ зліва направо і повертає індекс першого збігу:

Find.cpp
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string s = "the cat sat on the mat";

    // Знайти підрядок
    size_t p1 = s.find("the");
    cout << "перше \"the\": " << p1 << "\n"; // 0

    // Знайти символ
    size_t p2 = s.find('a');
    cout << "перша 'a':   " << p2 << "\n"; // 5

    // Пошук починаючи з позиції (другий аргумент)
    size_t p3 = s.find("the", 1); // пропустити позицію 0
    cout << "\"the\" після позиції 1: " << p3 << "\n"; // 15

    // Пошук підрядка, якого немає
    size_t p4 = s.find("dog");
    if (p4 == string::npos)
        cout << "\"dog\" не знайдено\n";

    return 0;
}
./Find
$ ./Find
перше "the": 0
перша 'a': 5
"the" після позиції 1: 15
"dog" не знайдено
Execution finished with exit code 0.

.rfind() — пошук останнього входження

.rfind() шукає справа наліво — повертає індекс останнього збігу:

Rfind.cpp
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string path = "/home/user/documents/report.2026.pdf";

    // Остання крапка — початок розширення
    size_t dot = path.rfind('.');
    if (dot != string::npos)
        cout << "Розширення: " << path.substr(dot + 1) << "\n"; // pdf

    // Остання '/' — початок імені файлу
    size_t slash = path.rfind('/');
    if (slash != string::npos)
        cout << "Файл: " << path.substr(slash + 1) << "\n";
        // report.2026.pdf

    // rfind з обмеженням позиції: шукати тільки до pos
    size_t dot2 = path.rfind('.', dot - 1); // попередня крапка
    if (dot2 != string::npos)
        cout << "Попередня крапка на: " << dot2 << "\n"; // 31

    return 0;
}
./Rfind
$ ./Rfind
Розширення: pdf
Файл: report.2026.pdf
Попередня крапка на: 31
Execution finished with exit code 0.

Пошук за набором символів: find_first_of та find_last_of

На відміну від find, який шукає конкретний підрядок, find_first_of шукає перший символ, що входить до вказаного набору:

FindFirstOf.cpp
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string s = "Hello, World! 123";

    // Перший символ із набору голосних
    size_t p1 = s.find_first_of("aeiouAEIOU");
    cout << "Перша голосна: '" << s[p1] << "' на позиції " << p1 << "\n";
    // 'e' на позиції 1

    // Перший розділовий знак або пробіл
    size_t p2 = s.find_first_of(" ,!?.");
    cout << "Перший розділювач: '" << s[p2] << "' на позиції " << p2 << "\n";
    // ',' на позиції 5

    // Перша цифра
    size_t p3 = s.find_first_of("0123456789");
    cout << "Перша цифра: '" << s[p3] << "' на позиції " << p3 << "\n";
    // '1' на позиції 14

    // Остання голосна
    size_t p4 = s.find_last_of("aeiouAEIOU");
    cout << "Остання голосна: '" << s[p4] << "' на позиції " << p4 << "\n";
    // 'o' на позиції 8

    return 0;
}
./FindFirstOf
$ ./FindFirstOf
Перша голосна: 'e' на позиції 1
Перший розділювач: ',' на позиції 5
Перша цифра: '1' на позиції 14
Остання голосна: 'o' на позиції 8
Execution finished with exit code 0.
Рядок-аргумент у find_first_of("aeiou") — це набір символів, а не підрядок для пошуку. Метод шукає будь-який символ із цього набору, а не їх послідовність. Щоб знайти саме підрядок "ae", використовуйте find("ae").

Пошук за виключенням: find_first_not_of та find_last_not_of

Методи find_first_not_of / find_last_not_of — дзеркальні: знаходять перший символ, що не входить до вказаного набору:

FindFirstNotOf.cpp
#include <iostream>
#include <string>

using namespace std;

int main()
{
    // Типовий приклад: trim — видалити пробіли з країв
    string s = "   Hello, World!   ";

    size_t start = s.find_first_not_of(" \t\r\n");
    size_t end   = s.find_last_not_of(" \t\r\n");

    if (start == string::npos)
    {
        cout << "Рядок порожній або лише пробіли\n";
    }
    else
    {
        string trimmed = s.substr(start, end - start + 1);
        cout << "'" << trimmed << "'\n"; // 'Hello, World!'
    }

    // Перевірка: чи рядок містить лише цифри?
    string digits = "12345";
    string mixed  = "123abc";

    bool onlyDigits1 = digits.find_first_not_of("0123456789") == string::npos;
    bool onlyDigits2 = mixed.find_first_not_of("0123456789") == string::npos;

    cout << boolalpha;
    cout << "\"" << digits << "\" — лише цифри: " << onlyDigits1 << "\n"; // true
    cout << "\"" << mixed  << "\" — лише цифри: " << onlyDigits2 << "\n"; // false

    return 0;
}
./FindFirstNotOf
$ ./FindFirstNotOf
'Hello, World!'
"12345" — лише цифри: true
"123abc" — лише цифри: false
Execution finished with exit code 0.

Зведена таблиця методів пошуку

s.find(what, pos=0)
size_t
Перше входження підрядка або символу, починаючи з pos. Повертає npos якщо не знайдено.
s.rfind(what, pos=npos)
size_t
Останнє входження підрядка або символу, шукаючи назад від pos. Повертає npos якщо не знайдено.
s.find_first_of(chars, pos=0)
size_t
Перший символ, що входить до набору chars. Корисно для пошуку будь-якого роздільника.
s.find_last_of(chars, pos=npos)
size_t
Останній символ, що входить до набору chars.
s.find_first_not_of(chars, pos=0)
size_t
Перший символ, що не входить до набору chars. Класичне використання — trim зліва.
s.find_last_not_of(chars, pos=npos)
size_t
Останній символ, що не входить до набору chars. Класичне використання — trim справа.

Практичні патерни

Знайти всі входження підрядка

Щоб знайти всі позиції входження підрядка, використовується цикл з find(..., lastPos + length):

FindAllOccurrences.cpp
#include <iostream>
#include <string>
#include <vector>

using namespace std;

vector<size_t> findAll(const string& s, const string& sub)
{
    vector<size_t> positions;
    size_t pos = 0;

    while ((pos = s.find(sub, pos)) != string::npos)
    {
        positions.push_back(pos);
        pos += sub.length(); // рухаємось далі, щоб не зависнути на тому ж місці
    }

    return positions;
}

int main()
{
    string text = "the cat sat on the mat near the hat";
    vector<size_t> positions = findAll(text, "the");

    cout << "\"the\" знайдено " << positions.size() << " разів:\n";
    for (size_t p : positions)
        cout << "  позиція " << p << ": \"" << text.substr(p, 10) << "...\"\n";

    return 0;
}
./FindAllOccurrences
$ ./FindAll
"the" знайдено 3 разів:
позиція 0: "the cat sa..."
позиція 15: "the mat ne..."
позиція 27: "the hat..."
Execution finished with exit code 0.

Розбиття рядка: split

Один із найпоширеніших патернів — розбиття рядка за роздільником:

Split.cpp
#include <iostream>
#include <string>
#include <vector>

using namespace std;

vector<string> split(const string& s, char delimiter)
{
    vector<string> tokens;
    size_t start = 0;
    size_t pos   = s.find(delimiter);

    while (pos != string::npos)
    {
        tokens.push_back(s.substr(start, pos - start));
        start = pos + 1;
        pos   = s.find(delimiter, start);
    }

    // Не забути останній токен (після останнього роздільника)
    tokens.push_back(s.substr(start));

    return tokens;
}

int main()
{
    string csv = "Alice,30,Kyiv,Ukraine";
    vector<string> fields = split(csv, ',');

    cout << "Полів: " << fields.size() << "\n";
    for (size_t i = 0; i < fields.size(); ++i)
        cout << "  [" << i << "] = \"" << fields[i] << "\"\n";

    return 0;
}
./Split
$ ./Split
Полів: 4
[0] = "Alice"
[1] = "30"
[2] = "Kyiv"
[3] = "Ukraine"
Execution finished with exit code 0.

trim — видалення пробілів з країв

Trim.cpp
#include <iostream>
#include <string>

using namespace std;

string trim(const string& s)
{
    const string whitespace = " \t\r\n";

    size_t start = s.find_first_not_of(whitespace);
    if (start == string::npos)
        return ""; // рядок цілком із пробілів

    size_t end = s.find_last_not_of(whitespace);
    return s.substr(start, end - start + 1);
}

int main()
{
    cout << "'" << trim("  Hello, World!  ") << "'\n";
    cout << "'" << trim("\t\n test \t\n")    << "'\n";
    cout << "'" << trim("   ")               << "'\n";
    cout << "'" << trim("no spaces")         << "'\n";

    return 0;
}
./Trim
$ ./Trim
'Hello, World!'
'test'
''
'no spaces'
Execution finished with exit code 0.

contains та starts_with / ends_with

Часто потрібно лише перевірити наявність підрядка без збереження позиції. До C++23 — через find, з C++23 — через .contains():

Contains.cpp
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string email = "user@example.com";

    // contains (до C++23: через find)
    bool hasAt     = email.find('@')       != string::npos;
    bool hasDot    = email.find('.')       != string::npos;
    bool hasMalware= email.find("malware") != string::npos;

    cout << boolalpha;
    cout << "є '@':       " << hasAt      << "\n"; // true
    cout << "є '.':       " << hasDot     << "\n"; // true
    cout << "є 'malware': " << hasMalware << "\n"; // false

    // starts_with / ends_with (C++20)
    cout << "Починається з 'user': "
              << email.starts_with("user") << "\n"; // true
    cout << "Закінчується на '.com': "
              << email.ends_with(".com")   << "\n"; // true

    return 0;
}
./Contains
$ ./Contains
є '@': true
є '.': true
є 'malware': false
Починається з 'user': true
Закінчується на '.com': true
Execution finished with exit code 0.
У C++23 додано .contains(str) як скорочення find(str) != npos. Якщо компілятор підтримує стандарт C++23, використовуйте його — код стає виразнішим.

Парсинг формату key = value

Поєднання find, trim та substr дозволяє розібрати простий текстовий формат без бібліотек:

KeyValue.cpp
#include <iostream>
#include <string>

using namespace std;

string trim(const string& s)
{
    const string ws = " \t";
    size_t a = s.find_first_not_of(ws);
    if (a == string::npos) return "";
    size_t b = s.find_last_not_of(ws);
    return s.substr(a, b - a + 1);
}

int main()
{
    // Імітація рядків конфігураційного файлу
    const string lines[] = {
        "host     = localhost",
        "port     = 5432",
        "  database = myapp  ",
        "# це коментар",
        "timeout  = 30"
    };

    for (const string& line : lines)
    {
        string trimmed = trim(line);

        // Пропустити порожні рядки та коментарі
        if (trimmed.empty() || trimmed[0] == '#')
            continue;

        size_t eq = trimmed.find('=');
        if (eq == string::npos)
            continue; // немає '=' — некоректний рядок

        string key   = trim(trimmed.substr(0, eq));
        string value = trim(trimmed.substr(eq + 1));

        cout << "[" << key << "] = [" << value << "]\n";
    }

    return 0;
}
./KeyValue
$ ./KeyValue
[host] = [localhost]
[port] = [5432]
[database] = [myapp]
[timeout] = [30]
Execution finished with exit code 0.

Методи std::string vs алгоритми <algorithm>

Методи .find() та споріднені — не єдиний спосіб пошуку в рядку. std::string є контейнером і підтримує всі STL-алгоритми через ітератори:

StlVsStringSearch.cpp
#include <iostream>
#include <string>
#include <algorithm> // find, count, search

using namespace std;

int main()
{
    string s = "Hello, World!";

    // Знайти один символ через find
    auto it = find(s.begin(), s.end(), 'o');
    if (it != s.end())
        cout << "'o' на позиції: " << (it - s.begin()) << "\n"; // 4

    // Підрахувати кількість входжень символу через count
    int countL = static_cast<int>(count(s.begin(), s.end(), 'l'));
    cout << "Кількість 'l': " << countL << "\n"; // 3

    // Знайти підрядок через search (аналог find для ітераторів)
    string sub = "World";
    auto it2 = search(s.begin(), s.end(), sub.begin(), sub.end());
    if (it2 != s.end())
        cout << "\"World\" на позиції: " << (it2 - s.begin()) << "\n"; // 7

    // Знайти перший не-символ слова через find_if
    auto notAlpha = find_if(s.begin(), s.end(),
        [](unsigned char c) { return !isalpha(c); });
    if (notAlpha != s.end())
        cout << "Перший не-літерний: '" << *notAlpha
                  << "' на позиції " << (notAlpha - s.begin()) << "\n"; // ',' на 5

    return 0;
}
./StlVsStringSearch
$ ./StlSearch
'o' на позиції: 4
Кількість 'l': 3
"World" на позиції: 7
Перший не-літерний: ',' на позиції 5
Execution finished with exit code 0.

Коли що обирати:

ЗадачаКращий варіант
Знайти підрядокs.find(sub) — зручніше, читабельніше
Знайти символ із наборуs.find_first_of(chars)
Підрахувати символиstd::count(s.begin(), s.end(), ch)
Знайти за умовою (предикатом)std::find_if(s.begin(), s.end(), pred)
Знайти підрядок (ітераторний стиль)std::search(...)

Практика

Рівень 1 — Валідація рядка

Напишіть функцію isValidIdentifier(const std::string& s), що перевіряє, чи є рядок коректним ідентифікатором C++ (починається з літери або _, містить лише літери, цифри та _).

Рівень 2 — Функція split із рядком-роздільником

Узагальніть функцію split зі зразка вище: роздільником має бути не символ, а підрядок довільної довжини.

Рівень 3 — Парсер .ini / .env конфігурації

Напишіть функцію, що зчитує конфігурацію з рядка у форматі .env: KEY=value, ігноруючи порожні рядки, коментарі (#) та пробіли навколо =. Результат повертається як вектор пар {key, value}.


Резюме

std::string::npos

Спеціальна константа SIZE_MAX — сигнал «не знайдено». Усі методи пошуку повертають її при невдачі. Перевірка: pos != std::string::npos. Не порівнюйте з -1 чи 0size_t беззнаковий.

find та rfind

find(what, pos) — перше входження зліва направо. rfind(what, pos) — останнє (справа наліво). Обидва приймають підрядок або символ. pos дозволяє продовжити пошук з довільного місця.

find_first/last_of

Шукають перший (або останній) символ із набору символів. find_first_of(".,;!") — перший знак пунктуації. find_last_of("/\\") — остання косая риска в шляху.

find_first/last_not_of

Шукають перший (або останній) символ, що не входить до набору. Основне застосування: trim (видалення пробілів з країв), валідація (чи рядок містить лише дозволені символи).

Практичні патерни

findAll — цикл find(..., pos + sub.len()). splitfind + substr у циклі. trimfind_first_not_of + find_last_not_of + substr. containsfind != npos; у C++23: .contains().

STL алгоритми

Для пошуку за предикатом — std::find_if. Для підрахунку — std::count. Для пошуку підрядка через ітератори — std::search. Методи рядка зручніші для підрядків; алгоритми — коли потрібна гнучкість предиката.

Що далі? Наступна стаття — std::string_view (C++17): легковагий «погляд» на рядок без копіювання, його переваги при передачі в функції, а також важливі обмеження та правила безпечного використання.

Copyright © 2026