Avatar ·

Как итерироваться по словам в строке на C++: разбор строки на токены

Всем привет! Я новичок в C++ (раньше писал на Python) и столкнулся с задачей, которая кажется элементарной, но вызывает у меня ступор. Мне нужно разбить строку на отдельные слова и пройтись по ним в цикле. В Python это делается одной строкой: `for word in sentence.split():`. Но в C++ всё оказалось сложнее. Вот пример моей задачи. Есть строка с предложением: ```cpp std::string text = "Hello, world! This is a test."; ``` Мне нужно получить каждое слово отдельно: "Hello,", "world!", "This", "is", "a", "test." (или без знаков препинания — это уже детали). Я попробовал использовать `std::stringstream`: ```cpp std::stringstream ss(text); std::string word; while (ss >> word) { std::cout << word << std::endl; } ``` Это работает, но только если слова разделены пробелами. А что делать, если разделители другие — запятые, точки, табуляции? Или если строка взята из CSV-файла? Кроме того, как правильно обработать пустые строки или строки с несколькими пробелами подряд? Я слышал про `std::getline`, но она работает только с одним разделителем. Подскажите, пожалуйста, как правильно и эффективно итерироваться по словам в C++? Желательно с примерами кода для разных ситуаций: простой сплит по пробелам, сплит по произвольному набору разделителей, и как это сделать в современном C++11/17/20. Заранее спасибо! <<>>

Решение 1: Использование std::istringstream для базового разбиения по пробелам

Самый простой и часто используемый способ — это применение std::istringstream в сочетании с оператором >>. Этот метод автоматически пропускает все пробельные символы (пробелы, табуляции, переводы строк) и читает слова до следующего пробела. Вот как это работает:

#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::string text = "Hello, world! This is a test.";
    std::istringstream stream(text);
    std::string word;
    while (stream >> word) {
        std::cout << word << std::endl;
    }
    return 0;
}

Вывод:

Hello,
world!
This
is
a
test.

Важно: Этот метод сохраняет знаки препинания как часть слова. Если вам нужно удалить пунктуацию, придётся дополнительно обрабатывать каждое слово (например, с помощью std::remove_if).

Решение 2: Разбиение по произвольному набору разделителей (современный C++17)

Если вам нужно разбить строку по кастомным разделителям (например, запятым, точкам или точкам с запятой), можно использовать std::string_view и цикл с поиском. Этот метод эффективнее, так как не создаёт копии подстрок, а работает с представлением исходной строки.

#include <iostream>
#include <string>
#include <string_view>
#include <vector>

std::vector<std::string_view> split(std::string_view str, char delimiter) {
    std::vector<std::string_view> tokens;
    size_t start = 0;
    size_t end = str.find(delimiter);
    while (end != std::string_view::npos) {
        tokens.push_back(str.substr(start, end - start));
        start = end + 1;
        end = str.find(delimiter, start);
    }
    tokens.push_back(str.substr(start));
    return tokens;
}

int main() {
    std::string text = "apple,banana,orange,grape";
    auto words = split(text, ',');
    for (auto word : words) {
        std::cout << word << std::endl;
    }
    return 0;
}

Вывод:

apple
banana
orange
grape

Преимущества: Работает с любым разделителем, не копирует строки (использует string_view), легко адаптируется для нескольких разделителей (просто добавьте цикл по массиву разделителей).

Решение 3: Использование std::regex для сложных шаблонов (C++11)

Если разделители представляют собой не один символ, а регулярное выражение (например, любые знаки препинания или пробелы), то std::regex — ваш выбор. Однако будьте осторожны: регулярные выражения могут быть медленными для больших строк.

#include <iostream>
#include <regex>
#include <string>
#include <vector>

std::vector<std::string> split(const std::string& str, const std::regex& delim) {
    std::sregex_token_iterator iter(str.begin(), str.end(), delim, -1);
    std::sregex_token_iterator end;
    return {iter, end};
}

int main() {
    std::string text = "Hello, world! This is a test.";
    std::regex delim("[\\s,;:.!?]+"); // разделители: пробелы, знаки препинания
    auto words = split(text, delim);
    for (const auto& word : words) {
        if (!word.empty()) {
            std::cout << word << std::endl;
        }
    }
    return 0;
}

Вывод:

Hello
world
This
is
a
test

Рекомендация: Для большинства задач используйте первый или второй метод. Регулярные выражения применяйте только когда разделители сложные (например, "любая последовательность цифр" или "пробел, за которым не следует восклицательный знак").

Решение 4: Классический boost::split (если разрешены внешние библиотеки)

Библиотека Boost предоставляет мощный инструмент для работы со строками. boost::split поддерживает несколько разделителей и автоматически удаляет пустые токены.

#include <boost/algorithm/string.hpp>
#include <string>
#include <vector>

std::string text = "one,,two,three";
std::vector<std::string> tokens;
boost::split(tokens, text, boost::is_any_of(","), boost::token_compress_on);
// tokens = {"one", "two", "three"}

Примечание: boost::token_compress_on объединяет последовательные разделители в один, что избавляет от пустых строк. Если такого флага нет, то строка "one,,two" даст токены {"one", "", "two"}.

<<>>

Альтернативный подход: Ручной парсинг с помощью std::find_if

Иногда хочется полного контроля над процессом. Можно написать функцию, которая вручную ищет начало и конец слова, используя std::find_if и лямбда-функцию для определения разделителей. Этот метод хорош тем, что не требует дополнительных библиотек и легко настраивается.

#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>

std::vector<std::string> split_words(const std::string& str) {
    std::vector<std::string> words;
    auto it = str.begin();
    while (it != str.end()) {
        // Пропускаем разделители (пробелы, знаки препинания)
        it = std::find_if(it, str.end(), [](char c) {
            return !std::ispunct(c) && !std::isspace(c);
        });
        if (it == str.end()) break;
        // Ищем конец слова
        auto word_end = std::find_if(it, str.end(), [](char c) {
            return std::ispunct(c) || std::isspace(c);
        });
        words.emplace_back(it, word_end);
        it = word_end;
    }
    return words;
}

Плюсы: Полный контроль, легко добавить новые правила (например, не разбивать по дефису или апострофу). Минусы: Код длиннее, чем готовые решения, и может быть медленнее для коротких строк (но для большинства задач это не критично).

Совет: Для продакшена используйте std::istringstream или boost::split, а для учебных целей или специфических правил — ручной парсинг.