Пошук у std::string: find, npos та практичні патерни
Пошук у std::string
Три задачі, одна тема
Погляньте на три типові запити:
#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;
}
Три методи — три різних питання про рядок. У цій статті ми розберемо весь арсенал методів пошуку 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-бітних системах). Це число значно більше за будь-який реальний розмір рядка, тому воно є безпечним «сигнальним» значенням.
#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;
}
pos >= 0 чи pos != -1 — size_t беззнаковий, від'ємних значень не існує. Завжди порівнюйте з std::string::npos: if (pos != std::string::npos).Методи find та rfind
.find() — пошук першого входження
.find() шукає підрядок або символ зліва направо і повертає індекс першого збігу:
#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;
}
.rfind() — пошук останнього входження
.rfind() шукає справа наліво — повертає індекс останнього збігу:
#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;
}
Пошук за набором символів: find_first_of та find_last_of
На відміну від find, який шукає конкретний підрядок, find_first_of шукає перший символ, що входить до вказаного набору:
#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;
}
find_first_of("aeiou") — це набір символів, а не підрядок для пошуку. Метод шукає будь-який символ із цього набору, а не їх послідовність. Щоб знайти саме підрядок "ae", використовуйте find("ae").Пошук за виключенням: find_first_not_of та find_last_not_of
Методи find_first_not_of / find_last_not_of — дзеркальні: знаходять перший символ, що не входить до вказаного набору:
#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;
}
Зведена таблиця методів пошуку
pos. Повертає npos якщо не знайдено.pos. Повертає npos якщо не знайдено.chars. Корисно для пошуку будь-якого роздільника.chars.chars. Класичне використання — trim зліва.chars. Класичне використання — trim справа.Практичні патерни
Знайти всі входження підрядка
Щоб знайти всі позиції входження підрядка, використовується цикл з find(..., lastPos + length):
#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;
}
Розбиття рядка: split
Один із найпоширеніших патернів — розбиття рядка за роздільником:
#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;
}
trim — видалення пробілів з країв
#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;
}
contains та starts_with / ends_with
Часто потрібно лише перевірити наявність підрядка без збереження позиції. До C++23 — через find, з C++23 — через .contains():
#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(str) як скорочення find(str) != npos. Якщо компілятор підтримує стандарт C++23, використовуйте його — код стає виразнішим.Парсинг формату key = value
Поєднання find, trim та substr дозволяє розібрати простий текстовий формат без бібліотек:
#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;
}
Методи std::string vs алгоритми <algorithm>
Методи .find() та споріднені — не єдиний спосіб пошуку в рядку. std::string є контейнером і підтримує всі STL-алгоритми через ітератори:
#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;
}
Коли що обирати:
| Задача | Кращий варіант |
|---|---|
| Знайти підрядок | 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++ (починається з літери або _, містить лише літери, цифри та _).
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
bool isValidIdentifier(const string& s)
{
if (s.empty()) return false;
// Перший символ: лише літера або '_'
if (!isalpha(static_cast<unsigned char>(s[0])) && s[0] != '_')
return false;
// Решта символів: літери, цифри або '_'
// find_first_not_of перевіряє, чи є хоч один «чужий» символ
const string allowed =
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789_";
return s.find_first_not_of(allowed) == string::npos;
}
int main()
{
cout << boolalpha;
cout << isValidIdentifier("myVar") << "\n"; // true
cout << isValidIdentifier("_count") << "\n"; // true
cout << isValidIdentifier("2fast") << "\n"; // false (починається з цифри)
cout << isValidIdentifier("my-var") << "\n"; // false (тире заборонено)
cout << isValidIdentifier("") << "\n"; // false (порожній)
return 0;
}
Рівень 2 — Функція split із рядком-роздільником
Узагальніть функцію split зі зразка вище: роздільником має бути не символ, а підрядок довільної довжини.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
vector<string> split(const string& s,
const string& delimiter)
{
vector<string> tokens;
if (delimiter.empty())
{
tokens.push_back(s);
return 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 + delimiter.length();
pos = s.find(delimiter, start);
}
tokens.push_back(s.substr(start));
return tokens;
}
int main()
{
// Розбити HTTP-заголовок за ": "
string header = "Content-Type: text/html; charset=utf-8";
auto parts = split(header, ": ");
for (const auto& p : parts)
cout << "[" << p << "]\n";
cout << "\n";
// Розбити SQL за " AND "
string cond = "age > 18 AND city = 'Kyiv' AND active = 1";
auto clauses = split(cond, " AND ");
for (const auto& c : clauses)
cout << " * " << c << "\n";
return 0;
}
Рівень 3 — Парсер .ini / .env конфігурації
Напишіть функцію, що зчитує конфігурацію з рядка у форматі .env: KEY=value, ігноруючи порожні рядки, коментарі (#) та пробіли навколо =. Результат повертається як вектор пар {key, value}.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct ConfigEntry
{
string key;
string value;
};
string trim(const string& s)
{
const string ws = " \t\r\n";
size_t a = s.find_first_not_of(ws);
if (a == string::npos) return "";
return s.substr(a, s.find_last_not_of(ws) - a + 1);
}
// Розбити багаторядковий текст на рядки
vector<string> splitLines(const string& text)
{
vector<string> lines;
size_t start = 0, pos = text.find('\n');
while (pos != string::npos)
{
lines.push_back(text.substr(start, pos - start));
start = pos + 1;
pos = text.find('\n', start);
}
lines.push_back(text.substr(start));
return lines;
}
vector<ConfigEntry> parseEnv(const string& text)
{
vector<ConfigEntry> result;
for (const string& rawLine : splitLines(text))
{
string line = trim(rawLine);
if (line.empty() || line[0] == '#')
continue; // порожній рядок або коментар
size_t eq = line.find('=');
if (eq == string::npos || eq == 0)
continue; // немає '=' або ключ порожній
ConfigEntry entry;
entry.key = trim(line.substr(0, eq));
entry.value = trim(line.substr(eq + 1));
// Прибрати лапки зі значення: "value" → value
if (entry.value.length() >= 2
&& entry.value.front() == '"'
&& entry.value.back() == '"')
{
entry.value = entry.value.substr(1, entry.value.length() - 2);
}
result.push_back(entry);
}
return result;
}
int main()
{
const string envText =
"# Database settings\n"
"DB_HOST = localhost\n"
"DB_PORT = 5432\n"
"DB_NAME = \"myapp_db\"\n"
"\n"
"# App settings\n"
"APP_DEBUG = true\n"
" APP_SECRET = \"s3cr3t_key\" \n";
auto config = parseEnv(envText);
for (const auto& entry : config)
cout << entry.key << " => " << entry.value << "\n";
return 0;
}
Резюме
std::string::npos
SIZE_MAX — сигнал «не знайдено». Усі методи пошуку повертають її при невдачі. Перевірка: pos != std::string::npos. Не порівнюйте з -1 чи 0 — size_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 (видалення пробілів з країв), валідація (чи рядок містить лише дозволені символи).Практичні патерни
find(..., pos + sub.len()). split — find + substr у циклі. trim — find_first_not_of + find_last_not_of + substr. contains — find != npos; у C++23: .contains().STL алгоритми
std::find_if. Для підрахунку — std::count. Для пошуку підрядка через ітератори — std::search. Методи рядка зручніші для підрядків; алгоритми — коли потрібна гнучкість предиката.Що далі? Наступна стаття — std::string_view (C++17): легковагий «погляд» на рядок без копіювання, його переваги при передачі в функції, а також важливі обмеження та правила безпечного використання.
Модифікація std::string: присвоювання, додавання, вставка, видалення та заміна
Методи зміни вмісту рядка: assign(), append(), push_back(), insert(), erase(), replace(), resize(). Витяг підрядка через substr(). Порівняння рядків: оператори та метод compare().
std::string_view: невласницький погляд на рядок без копіювання
Клас std::string_view (C++17): внутрішня модель {ptr, length}, створення з const char* та std::string, методи read-only доступу, remove_prefix/remove_suffix, відсутність нуль-термінатора. Dangling view — найнебезпечніша пастка. Коли обирати string_view, а коли const std::string&.