Вступление

Распространенной задачей в программировании является разделение строки с разделителями на массив токенов. Например, может потребоваться разбить строку, содержащую пробелы, на массив слов. Это одна из областей, в которой языки программирования, такие как Java и Python, превосходят C ++, поскольку оба этих языка программирования включают поддержку этого в своих стандартных библиотеках, а C ++ - нет. Однако в C ++ эту задачу можно решить разными способами.

В Java эту задачу можно решить с помощью метода String.split следующим образом.

String Str = "The quick brown fox jumped over the lazy dog.";
String[] Results = Str.split(" ");

В Python эту задачу можно решить с помощью метода str.split следующим образом.

Str = "The quick brown fox jumped over the lazy dog."
Results = Str.split()

В C ++ эта задача не так проста. Это все еще можно сделать множеством разных способов.

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

Один из вариантов - использовать библиотеку времени выполнения C. Следующие функции библиотеки времени выполнения C могут использоваться для разделения строки.

К сожалению, между платформами существуют различные различия. Это требует использования директив препроцессора C для определения того, что подходит для текущей платформы.

Следующий код демонстрирует эту технику.

char* FindToken(
    char* str, const char* delim, char** saveptr)
{
#if (_SVID_SOURCE || _BSD_SOURCE || _POSIX_C_SOURCE >= 1 \
  || _XOPEN_SOURCE || _POSIX_SOURCE)
    return ::strtok_r(str, delim, saveptr);
#elif defined(_MSC_VER) && (_MSC_VER >= 1800)
    return strtok_s(token, delim, saveptr);
#else
    return std::strtok(token, delim);
#endif
}
wchar_t* FindToken(
    wchar_t* token, const wchar_t* delim, wchar_t** saveptr)
{
#if ( (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) \
  || (defined(__cplusplus) && (__cplusplus >= 201103L)) )
    return std::wcstok(token, delim, saveptr);
#elif defined(_MSC_VER) && (_MSC_VER >= 1800)
    return wcstok_s(token, delim, saveptr);
#else
    return std::wcstok(token, delim);
#endif
}
char* CopyString(char* destination, const char* source)
{
    return std::strcpy(destination, source);
}
wchar_t* CopyString(wchar_t* destination, const wchar_t* source)
{
    return std::wcscpy(destination, source);
}
template <class charType>
size_t splitWithFindToken(
    const std::basic_string<charType>& str,
    const std::basic_string<charType>& delim,
    std::vector< std::basic_string<charType> >& tokens)
{
    std::unique_ptr<charType[]> ptr = std::make_unique<charType[]>(
        str.length() + 1);
    memset(ptr.get(), 0, (str.length() + 1) * sizeof(charType));
    CopyString(ptr.get(), str.c_str());
    charType* saveptr;
    charType* token = FindToken(ptr.get(), delim.c_str(), &saveptr);
    while (token != nullptr)
    {
        tokens.push_back(token);
        token = FindToken(nullptr, delim.c_str(), &saveptr);
    }
    return tokens.size();
}

Использование класса basic_istringstream

Решение 1. std :: istream_iterator

Пожалуй, самый простой способ решить эту задачу - использовать класс basic_istringstream следующим образом.

template <class charType>
size_t splitWithStringStream(
    const std::basic_string<charType>& str,
    std::vector< std::basic_string<charType> >& tokens)
{
    typedef std::basic_string<charType> my_string;
    typedef std::vector< std::basic_string<charType> > my_vector;
    typedef std::basic_istringstream<
        charType, std::char_traits<charType> >
        my_istringstream;
    typedef std::istream_iterator<
        std::basic_string<charType>, charType,
        std::char_traits<charType> >
        my_istream_iterator;
    tokens.clear();
    if (str.empty())
    {
        return 0;
    }
    my_istringstream iss(str);
    std::copy(
        my_istream_iterator{iss}, my_istream_iterator(),
        std::back_inserter<my_vector>(tokens));
    return tokens.size();
}

Функцию splitWithStringStream можно использовать следующим образом.

std::string str("The quick brown fox jumped over the lazy dog.");
std::vector<std::string> tokens;
size_t s = splitWithStringStream(str, tokens);

Функция splitWithStringStream имеет то преимущество, что не использует ничего, кроме функций, которые являются частью стандартной библиотеки C ++. Чтобы использовать его, вам просто нужно включить следующие заголовки стандартной библиотеки C ++: алгоритм, итератор, sstream, строка и вектор.

Альтернативный вариант функции следующий.

template <class charType>
size_t splitWithStringStream1(
    const std::basic_string<charType>& str,
    std::vector< std::basic_string<charType> >& tokens)
{
    typedef std::basic_string<charType> my_string;
    typedef std::vector< std::basic_string<charType> > my_vector;
    typedef std::basic_istringstream<
        charType, std::char_traits<charType> >
        my_istringstream;
    typedef std::istream_iterator<
        std::basic_string<charType>, charType,
        std::char_traits<charType> >
        my_istream_iterator;
    tokens.clear();
    if (str.empty())
    {
        return 0;
    }
    my_istringstream iss(str);
    std::vector<my_string> results(
        my_istream_iterator{iss}, my_istream_iterator());
    tokens.swap(results);
    return tokens.size();
}

У функций splitWithStringStream и splitWithStringStream1 есть два недостатка. Во-первых, функции потенциально неэффективны и медленны, поскольку вся строка копируется в поток, который занимает столько же памяти, сколько и строка. Во-вторых, они поддерживают только строки, разделенные пробелами.

Решение 2. std :: getline

Следующая функция позволяет использовать в качестве разделителя символ, отличный от пробела.

template<typename charType>
size_t splitWithGetLine(
    const std::basic_string<charType>& str,
    const charType delim,
    std::vector< std::basic_string<charType> >& tokens)
{
    typedef std::basic_string<charType> my_string;
    typedef std::basic_istringstream<
        charType, std::char_traits<charType> >
        my_istringstream;
    tokens.clear();
    if (str.empty())
    {
       return 0;
    }
    my_istringstream iss(str);
    my_string token;
    while (std::getline(iss, token, delim))
    {
        tokens.push_back(token);
    }
    return tokens.size();
}

Эту функцию можно использовать следующим образом.

std::wstring str(L"This is a test.||This is only a test.|This concludes this test.");
std::vector<std::wstring> tokens;
size_t s = splitWithGetLine(str, L'|', tokens);

Обратите внимание, что это решение не пропускает пустые токены, поэтому в приведенном выше примере будут токены, содержащие четыре элемента, один из которых будет пустой строкой.

Эта функция, как и функции splitWithStringStream и splitWithStringStream1, потенциально неэффективна и медленна. Это позволяет указать символ-разделитель. Однако он поддерживает только один ограничивающий символ. Эта функция не поддерживает строки, в которых можно использовать несколько символов-разделителей.

Чтобы использовать функцию splitWithGetLine, вам просто нужно включить следующие заголовки стандартной библиотеки C ++: алгоритм, итератор, sstream, строка и вектор.

Использование только членов класса basic_string

Решение 1

Эту задачу можно выполнить, используя только функции-члены класса basic_string. Следующая функция позволяет указать символ-разделитель и использует только члены find_first_not_of, find и substr класса basic_string. Функция также имеет необязательные параметры, которые позволяют указать, что пустые токены должны игнорироваться, и указать максимальное количество сегментов, на которые должна быть разбита строка.

template<typename charType>
size_t splitWithBasicString(
    const std::basic_string<charType>& str,
    const charType delim,
    std::vector< std::basic_string<charType> > &tokens,
    const bool trimEmpty = false,
    const size_t maxTokens = (size_t)(-1))
{
    typedef std::basic_string<charType> my_string;
    typedef typename my_string::size_type my_size_type;
    tokens.clear();
    if (str.empty())
    {
        return 0;
    }
    my_size_type len = str.length();
    // Skip delimiters at beginning.
    my_size_type left = str.find_first_not_of(delim, 0);
    size_t i = 1;
    if (!trimEmpty && left != 0)
    {
        tokens.push_back(my_string());
        ++i;
    }
    while (i < maxTokens)
    {
        my_size_type right = str.find(delim, left);
        if (right == my_string::npos)
        {
            break;
        }
        if (!trimEmpty || right - left > 0)
        {
            tokens.push_back(str.substr(left, right - left));
            ++i;
        }
        left = right + 1;
    }
    if (left < len)
    {
        tokens.push_back(str.substr(left));
    }
    return tokens.size();
}

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

Чтобы использовать функцию splitWithBasicString, вам просто нужно включить следующие заголовки стандартной библиотеки C ++: string и vector.

Решение 2

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

Следующая функция позволяет указать символ-разделитель и использует только члены find_first_not_of, find_first_of и substr класса basic_string. Функция также имеет необязательные параметры, которые позволяют указать, что пустые токены должны игнорироваться, и указать максимальное количество сегментов, на которые должна быть разбита строка.

template<typename charType>
size_t splitWithBasicString(
    const std::basic_string<charType>& str,
    const std::basic_string<charType>& delim,
    std::vector< std::basic_string<charType> >& tokens,
    const bool trimEmpty = false,
    const size_t maxTokens = (size_t)(-1))
{
    typedef std::basic_string<charType> my_string;
    typedef typename my_string::size_type my_size_type;
    tokens.clear();
    if (str.empty())
    {
       return 0;
    }
    my_size_type len = str.length();
    // Skip delimiters at beginning.
    my_size_type left = str.find_first_not_of(delim, 0);
    size_t i = 1;
    if (!trimEmpty && left != 0)
    {
        tokens.push_back(my_string());
        ++i;
    }
    while (i < maxTokens)
    {
        my_size_type right = str.find_first_of(delim, left);
        if (right == my_string::npos)
        {
           break;
        }
        if (!trimEmpty || right - left > 0)
        {
            tokens.push_back(str.substr(left, right - left));
            ++i;
        }
        left = right + 1;
    }
    if (left < len)
    {
       tokens.push_back(str.substr(left));
    }
    return tokens.size();
}

Использование Boost

Boost - это набор проверенных кросс-платформенных библиотек C ++ с открытым исходным кодом, которые предназначены для дополнения и расширения стандартной библиотеки C ++. Boost предоставляет как минимум два метода разделения строки.

Решение 1

Один из вариантов - использовать функцию boost :: algorithm :: split в Библиотеке алгоритмов Boost String.

Чтобы использовать функцию разделения, просто включите boost / algorithm / string.hpp, а затем вызовите функцию следующим образом.

std::string str(" The  quick brown fox\tjumped over the lazy dog.");
std::vector<std::string> strs;
boost::split(strs, str, boost::is_any_of("\t "));

Решение 2

Другой вариант - использовать Библиотеку Boost Tokenizer. Чтобы использовать библиотеку Boost Tokenizer, просто включите boost / tokenizer.hpp. Затем вы можете использовать Boost Tokenizer следующим образом.

typedef boost::char_separator<char> my_separator;
typedef boost::tokenizer<my_separator> my_tokenizer;
std::string str(" The  quick brown fox\tjumped over the lazy dog.");
my_separator sep(" \t");
my_tokenizer tokens(str, sep);
my_tokenizer::iterator itEnd = tokens.end();
for (my_tokenizer::iterator it = tokens.begin(); it != itEnd; ++it)
{
    std::cout << *it << std::endl;
}

Использование библиотеки C ++ String Toolkit

Другой вариант - использовать C ++ String Toolkit Library. В следующем примере показано, как можно использовать функцию strtk :: parse для разделения строки.

std::string str("The quick brown fox jumped over the lazy dog.");
std::vector<std::string> tokens;
strtk::parse(str, " ", tokens);

Другие варианты

Конечно, есть много других вариантов. Не стесняйтесь обращаться к веб-страницам, перечисленным в разделе ссылок ниже, для многих других вариантов.

Резюме

В этой статье я рассмотрел несколько вариантов разделения строк в C ++.

Код для классов basic_istringstream, basic_string и Boost вместе с полным примером, демонстрирующим использование функций, можно найти на Ideone.

использованная литература

Первоначально опубликовано на yekneb.com 24 октября 2018 г.