按字符分割字符串

80

我知道这是一个相当简单的问题,但是我想彻底地解决它

我想使用一个字符作为分隔符来将字符串拆分成数组。(很像C#中著名的.Split()函数) 我当然可以采用暴力方法,但我想知道是否有比这更好的方法。

到目前为止,我已经搜索了一些方法,可能最接近的解决方案是使用strtok(),但由于它不便(需要将您的字符串转换为char数组等),我不喜欢使用它。是否有更简单的方法来实现这个功能?

注意:我想强调这点,因为人们可能会问“为什么暴力方法不起作用”。我的暴力解决方案是创建一个循环,并在其中使用substr()函数。不过,由于它需要指定起始位置和长度,在我要拆分日期时就失效了。因为用户可能输入 7/12/2012 或 07/3/2011,我无法在计算下一个'/'分隔符的位置之前确定长度。


可能是Splitting String C++的重复问题。 - Bo Persson
这个回答解决了你的问题吗?如何迭代字符串中的单词? - xskxzr
15个回答

158

使用向量、字符串和stringstream。有点繁琐,但它能解决问题。

#include <string>
#include <vector>
#include <sstream>

std::stringstream test("this_is_a_test_string");
std::string segment;
std::vector<std::string> seglist;

while(std::getline(test, segment, '_'))
{
   seglist.push_back(segment);
}

这会得到一个与原向量内容相同的向量。

std::vector<std::string> seglist{ "this", "is", "a", "test", "string" };

2
实际上,这种方法正是我正在寻找的。非常易于理解,没有使用外部库,非常直接。感谢@thelazydeveloper! - Ali
如果您想要提高性能,可以添加以下代码: seglist.reserve(std::count_if(str.begin(), str.end(), [&](char c) { return c == splitChar; }) + (str.empty() ? 1 : 0)); 其中,原始字符串存储在 str 中。 - Jarek C
1
与其使用while (std::getline(test, segment, '_')),不如改用while (!std::getline(test, segment, '_').eof())更好。 - Makonede
如果我输入“__”,它会在向量中添加一个空字符串。这正是我想要的。谢谢。 - Gabrial David
这在空白部分上表现不佳。像这样分割空字符串会导致一个空向量,即 "" -> {} 而不是 {""}。如果字符串以分隔符结尾,最后一个空字符串将不会成为结果的一部分,即 "a_b_" -> {"a", "b"} 而不是 {"a", "b", ""}。这两个情况可能是你想要的,也可能不是,但它们绝对是意外的。 - undefined

29
Boost在algorithm/string.hpp中提供了你所需要的split()函数。
#include <boost/algorithm/string.hpp>

std::string sample = "07/3/2011";
std::vector<std::string> strs;
boost::split(strs, sample, boost::is_any_of("/"));

17

对于喜欢正则表达式的人来说,还有一种方法(C++11/boost)。就我个人而言,我非常喜欢使用正则表达式处理这种数据。因为你可以选择更加智能地确定“有效”数据的标准,所以它比仅仅使用定界符分割字符串要强大得多。

#include <string>
#include <algorithm>    // copy
#include <iterator>     // back_inserter
#include <regex>        // regex, sregex_token_iterator
#include <vector>

int main()
{
    std::string str = "08/04/2012";
    std::vector<std::string> tokens;
    std::regex re("\\d+");

    //start/end points of tokens in str
    std::sregex_token_iterator
        begin(str.begin(), str.end(), re),
        end;

    std::copy(begin, end, std::back_inserter(tokens));
}

1
所以你在代码中包含了整个正则表达式匹配器,只是为了拆分一个字符串。悲哀... - user6516765
2
@Dev 不,包括一个正则表达式匹配器可以更智能地确定什么是有效数据 - 例如选择数字,并允许其他分隔符,如点或连字符。 - Ben Cottrell
这对于二进制大小和总体效率都是不好的,但由于在这种情况下两者都不是任何关注点,所以我就不再继续了。 - user6516765
7
如果一个人对二进制文件大小有如此严格的限制,那么他们应该重新考虑是否使用C ++,或者至少不要使用标准库(如string / vector等),因为它们都会产生类似的影响。至于效率,最好的建议来自Donald Knuth——“过早优化是万恶之源”;换句话说,在进行优化之前,首要任务是确定问题是否存在,然后通过客观手段(如分析)确定原因,而不是浪费时间试图追踪每一种可能的微调。 - Ben Cottrell
2
@Dev 那我不得不想知道,即使提到它们的目的是什么。 - Ben Cottrell
显示剩余2条评论

9

由于还没有人发布此内容:的解决方案非常简单,使用ranges即可。您可以使用std::ranges::views::split来分割输入,然后将输入转换为std::stringstd::string_view元素。

#include <ranges>


...

// The input to transform
const auto str = std::string{"Hello World"};

// Function to transform a range into a std::string
// Replace this with 'std::string_view' to make it a view instead.
auto to_string = [](auto&& r) -> std::string {
    const auto data = &*r.begin();
    const auto size = static_cast<std::size_t>(std::ranges::distance(r));

    return std::string{data, size};
};

const auto range = str | 
                   std::ranges::views::split(' ') | 
                   std::ranges::views::transform(to_string);

for (auto&& token : str | range) {
    // each 'token' is the split string
}

这种方法实际上可以用于构建几乎任何东西,甚至是一个简单的split函数,该函数返回一个std::vector<std::string>
auto split(const std::string& str, char delimiter) -> std::vector<std::string>
{
    const auto range = str | 
                       std::ranges::views::split(delimiter) | 
                       std::ranges::views::transform(to_string);

    return {std::ranges::begin(range), std::ranges::end(range)};
}

实时示例


  1. 为什么要使用 str | range 而不是 range
  2. 使用 transformto_string 是否必要?似乎可以将 token 声明为 string_view,这样就不需要使用 transform
  3. split_viewbeginend 函数是非 const 的,因此程序似乎不符合规范,因为 range for 循环使用了 const 范围。
- xskxzr
哦,我看到这里需要构建一个 string_view,从一个范围来说,这是C++23的一个特性。 - xskxzr
4
对比其他答案,这段文字阅读起来有些困难,不够清晰。 - user8143588

5

我本能地不喜欢stringstream,虽然我不确定为什么。今天,我写了这个函数,允许将std::string按任意字符或字符串拆分成向量。我知道这个问题很旧了,但我想分享一种替代方法来拆分std::string

此代码完全省略您从结果中拆分的字符串部分,尽管可以轻松修改以包括它们。

#include <string>
#include <vector>

void split(std::string str, std::string splitBy, std::vector<std::string>& tokens)
{
    /* Store the original string in the array, so we can loop the rest
     * of the algorithm. */
    tokens.push_back(str);

    // Store the split index in a 'size_t' (unsigned integer) type.
    size_t splitAt;
    // Store the size of what we're splicing out.
    size_t splitLen = splitBy.size();
    // Create a string for temporarily storing the fragment we're processing.
    std::string frag;
    // Loop infinitely - break is internal.
    while(true)
    {
        /* Store the last string in the vector, which is the only logical
         * candidate for processing. */
        frag = tokens.back();
        /* The index where the split is. */
        splitAt = frag.find(splitBy);
        // If we didn't find a new split point...
        if(splitAt == std::string::npos)
        {
            // Break the loop and (implicitly) return.
            break;
        }
        /* Put everything from the left side of the split where the string
         * being processed used to be. */
        tokens.back() = frag.substr(0, splitAt);
        /* Push everything from the right side of the split to the next empty
         * index in the vector. */
        tokens.push_back(frag.substr(splitAt+splitLen, frag.size()-(splitAt+splitLen)));
    }
}

使用时只需这样调用...

std::string foo = "This is some string I want to split by spaces.";
std::vector<std::string> results;
split(foo, " ", results);

现在您可以自由访问向量中的所有结果。就是这么简单 - 没有 stringstream,没有第三方库,也不需要退回到 C!

你有什么理由可以说明这样做会更好吗? - Newbyte
1
我也不是标准C++中某些东西的忠实粉丝(例如冗长可怕的流,但它们正在被fmtlib替换,所以我很高兴)。但当我可以写更少的代码时,我倾向于把这些感觉放在一边 - 至少开始时错误的机会大大降低了。 - paxdiablo

4

另一种可能性是将流注入使用特殊 ctype 面貌的区域设置。流使用 ctype 面貌来确定什么是“空白”,并将其视为分隔符。使用将分隔符字符归类为空格的 ctype 面貌,读取可以相当轻松。以下是实现面貌的一种方法:

struct field_reader: std::ctype<char> {

    field_reader(): std::ctype<char>(get_table()) {}

    static std::ctype_base::mask const* get_table() {
        static std::vector<std::ctype_base::mask> 
            rc(table_size, std::ctype_base::mask());

        // we'll assume dates are either a/b/c or a-b-c:
        rc['/'] = std::ctype_base::space;
        rc['-'] = std::ctype_base::space;
        return &rc[0];
    }
};

我们可以通过使用imbue来告诉流使用包含它的区域设置,然后从该流中读取数据:

std::istringstream in("07/3/2011");
in.imbue(std::locale(std::locale(), new field_reader);

有了这个,拆分就变得非常简单 -- 只需使用一对istream_iterator从包含在istringstream中的字符串中读取数据并初始化一个向量:

std::vector<std::string>((std::istream_iterator<std::string>(in),
                          std::istream_iterator<std::string>());

很明显,如果您只在一个地方使用它,这往往会过度。但是,如果您经常使用它,则可以大大帮助保持其余代码的整洁。

3

3
谢谢你的字符串查找提示。总是喜欢听到std的解决方案! - Ali

0

我一直在使用的一个解决方案是分割,它可以用于向量和列表。

#include <vector>
#include <string>
#include <list>

template< template<typename,typename> class Container, typename Separator >
Container<std::string,std::allocator<std::string> > split( const std::string& line, Separator sep ) {
    std::size_t pos = 0;
    std::size_t next = 0;
    Container<std::string,std::allocator<std::string> > fields;
    while ( next != std::string::npos ) {
        next = line.find_first_of( sep, pos );
        std::string field = next == std::string::npos ? line.substr(pos) : line.substr(pos,next-pos);
        fields.push_back(  field );
        pos = next + 1;
    }
    return fields;
}

int main() {
    auto res1 = split<std::vector>( "abc,def", ",:" );
    auto res2 = split<std::list>( "abc,def", ',' );
}

0

这段代码对我来说很有效,更易于理解,使用了向量和字符串处理。在这个方法中,我们使用find()和substr()函数来分割字符串。find()函数搜索定界符并返回第一次出现的位置。substr()函数根据给定的起始和结束位置从输入字符串中提取子字符串。我们遍历输入字符串,找到每个定界符的出现位置,然后从输入字符串的开头提取子字符串到定界符。然后将此子字符串推回字符串向量中。最后,我们打印出向量中的每个标记。

#include <iostream>
#include <vector>
#include <string>
using namespace std;

vector<string> split(string input, string delimiter){

    vector<string> tokens;
    size_t pos = 0;
    string token;

    while((pos = input.find(delimiter)) != string::npos){
        token = input.substr(0, pos);
        tokens.push_back(token);
        input.erase(0, pos + 1);
    }

    tokens.push_back(input);

    return tokens;
}

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接