使用strtok和std::string

60

我有一个字符串需要分割成标记。 但是 C strtok() 函数要求我的字符串是char*类型的。 如何简单地实现这个需求?

我已经尝试过:

token = strtok(str.c_str(), " "); 

它失败的原因是它将其转换为 const char* 而不是 char*


The reason for its failure is that it converts it into a const char* instead of a char*.

请查看此问题:https://dev59.com/cHVD5IYBdhLWcg3wNIvc#55680 - Ferruccio
14个回答

80
#include <iostream>
#include <string>
#include <sstream>
int main(){
    std::string myText("some-text-to-tokenize");
    std::istringstream iss(myText);
    std::string token;
    while (std::getline(iss, token, '-'))
    {
        std::cout << token << std::endl;
    }
    return 0;
}

或者,如之前提到的,使用boost以获得更多的灵活性。


strtok()支持多个分隔符,而getline不支持。有没有简单的方法可以绕过这个问题? - thegreatcoder
1
@thegreatcoder 我相信你可以使用regex_token_iterator来使用多个分隔符进行标记化。感谢你让我回忆起过去,我很久以前回答了这个问题 :) - Chris Blackwell

22

复制字符串,将其分词,然后释放它。

char *dup = strdup(str.c_str());
token = strtok(dup, " ");
free(dup);

3
更好的问题不是,当涉及到更好的本地选项时,为什么要使用strtok呢? - Kendall Helmstetter Gelner
1
不一定。如果问题的背景是维护一个脆弱的代码库,那么远离现有方法(例如我的strtok)比改变方法更具风险。在问题中没有更多的上下文情况下,我更愿意回答所问的问题。 - DocMax
如果提问者是新手,你应该在使用token之前避免使用free()... :-) - PhiLho
我对使用更强大的本地标记器比插入调用向其传递空块内存的库的新代码更安全持怀疑态度......这就是为什么我认为按照提问者所问的方式回答问题不是个好主意。 - Kendall Helmstetter Gelner
请注意,strtok() 不是线程安全或可重入的。在具有多个任务的程序中,应避免使用它。 - Colin D Bennett
另外,顺便提一下,我们应该注意到strdup()来自POSIX,这就是为什么最好不要使用它的原因。 - FanaticD

20
  1. If boost is available on your system (I think it's standard on most Linux distros these days), it has a Tokenizer class you can use.

  2. If not, then a quick Google turns up a hand-rolled tokenizer for std::string that you can probably just copy and paste. It's very short.

  3. And, if you don't like either of those, then here's a split() function I wrote to make my life easier. It'll break a string into pieces using any of the chars in "delim" as separators. Pieces are appended to the "parts" vector:

    void split(const string& str, const string& delim, vector<string>& parts) {
      size_t start, end = 0;
      while (end < str.size()) {
        start = end;
        while (start < str.size() && (delim.find(str[start]) != string::npos)) {
          start++;  // skip initial whitespace
        }
        end = start;
        while (end < str.size() && (delim.find(str[end]) == string::npos)) {
          end++; // skip to end of word
        }
        if (end-start != 0) {  // just ignore zero-length strings.
          parts.push_back(string(str, start, end-start));
        }
      }
    }
    

2
手写的链接已经损坏。 - definelicht

9

有一种更优雅的解决方案。

使用std::string,您可以使用resize()来分配一个适当大小的缓冲区,并使用&s[0]来获取指向内部缓冲区的指针。

此时,许多优秀的人会跳起来大喊。但这是事实。大约两年前,

图书馆工作组决定(在Lillehammer会议上)像std::vector一样,std::string也应该正式地、而不仅仅是在实践中拥有保证连续的缓冲区。

另一个问题是strtok()是否会增加字符串的大小。MSDN文档说:

每次调用strtok都会通过在该调用返回的标记后插入空字符来修改strToken。

但这是不正确的。实际上,该函数将第一个分隔符字符替换为\0。字符串的大小不会改变。如果我们有这个字符串:

one-two---three--four

我们最终会得到

one\0two\0--three\0-four

所以我的解决方案非常简单:


std::string str("some-text-to-split");
char seps[] = "-";
char *token;

token = strtok( &str[0], seps );
while( token != NULL )
{
   /* Do your thing */
   token = strtok( NULL, seps );
}

阅读关于std :: string是否有类似CString :: GetBuffer的功能的讨论。


1
strtok() 在以空字符结尾的字符串上工作,而 std::string 的缓冲区并不要求以空字符结尾。没有绕过 c_str() 的方法。 - SnakE
@SnakE std::string的缓冲区必须以空字符结尾。要求datac_str是相同的,并且data() + i == &operator[](i)对于每个i[0,size()]中都成立 - Alex Celeste
1
@Leushenko 你说的部分正确。自C++11起,才保证了空终止符。我已经在答案中添加了一条注释。我的编辑被接受后,我会取消我的-1。 - SnakE
这个黑客技巧不值得。这个“优雅”的解决方案会以几种方式破坏std::string对象。std::cout << str << " " << str.size(); std::cout << str.c_str()<< " " << strlen(str.c_str()); 之前:some-text-to-split 18 some-text-to-split 18 之后:sometexttosplit 18 some 4 - dmitri
以上代码中 "token = strtok(NULL, seps)" 的作用是什么?请回答,因为我尝试搜索这个用法但没有找到太多信息。 - Chandra Shekhar

3

在C++17中,str::string得到了一个重载的data()函数,它返回一个指向可修改缓冲区的指针,因此字符串可以直接在strtok中使用,而无需进行任何黑客攻击:

#include <string>
#include <iostream>
#include <cstring>
#include <cstdlib>

int main()
{
    ::std::string text{"pop dop rop"};
    char const * const psz_delimiter{" "};
    char * psz_token{::std::strtok(text.data(), psz_delimiter)};
    while(nullptr != psz_token)
    {
        ::std::cout << psz_token << ::std::endl;
        psz_token = std::strtok(nullptr, psz_delimiter);
    }
    return EXIT_SUCCESS;
}

输出

弹出
删除
读取


1
注意:原始的 std::string 将不再持有相同的值,因为 strtok 会将它找到的分隔符替换为原地的空终止符,而不是返回字符串的副本。如果你想保留原始字符串,请创建字符串的副本并将其传递给 strtok。 - user233009
1
@user233009 注意:如果strtok仅处理单个分隔符,则可以通过在每次迭代中放回分隔符并替换空终止符来保留字符串的原始值。 - user7860670

2

编辑:仅使用const cast是为了演示将strtok()应用于string::c_str()返回的指针时的效果。

不应该使用strtok(),因为它会修改已分词的字符串,这可能导致不希望的、甚至未定义的行为,因为C字符串“属于”字符串实例。

#include <string>
#include <iostream>

int main(int ac, char **av)
{
    std::string theString("hello world");
    std::cout << theString << " - " << theString.size() << std::endl;

    //--- this cast *only* to illustrate the effect of strtok() on std::string 
    char *token = strtok(const_cast<char  *>(theString.c_str()), " ");

    std::cout << theString << " - " << theString.size() << std::endl;

    return 0;
}

调用 strtok() 后,空格被“移除”或转换为不可打印字符,但字符串的长度保持不变。

>./a.out
hello world - 11
helloworld - 11

因此,您必须使用本地机制、字符串复制或前面提到的第三方库来解决问题。

强制转换去除const并不能帮助解决问题。它是const有其原因的。 - Martin York
1
@Martin York, @Sherm Pendley:你们是只看了结论还是只看了代码片段?我编辑了我的回答,以澄清我想要展示的内容。谢谢。 - philant
1
@Philippe - 是的,我只看了代码。很多人都会这样做,直接跳过解释,直接进入代码。也许在代码中加入解释作为注释是一个好主意?无论如何,我撤销了我的负面评价。 - Sherm Pendley
有人知道一个编译器(警告开关)或静态代码分析器,可以警告这类问题吗? - orbitcowboy

1

我猜测这是C或C++语言...

据我所知,strtok会用\0替换分隔符。这就是为什么它不能使用const字符串的原因。 如果字符串不是很大,为了“快速”解决这个问题,你可以使用strdup()。如果你需要保持字符串不变(正如const所建议的那样...),这是明智的选择。

另一方面,你可能想使用另一个分词器,也许是手写的,对给定的参数更加温和。


1

假设你所说的“string”是指C++中的std::string,那么你可以看一下Boost中的Tokenizer包。


0

如果您不介意使用开源代码,您可以使用https://github.com/EdgeCast/json_parser中的subbuffer和subparser类。原始字符串保持不变,没有分配和复制数据。我没有编译以下内容,因此可能会出现错误。

std::string input_string("hello world");
subbuffer input(input_string);
subparser flds(input, ' ', subparser::SKIP_EMPTY);
while (!flds.empty())
{
    subbuffer fld = flds.next();
    // do something with fld
}

// or if you know it is only two fields
subbuffer fld1 = input.before(' ');
subbuffer fld2 = input.sub(fld1.length() + 1).ltrim(' ');

0

Chris的答案在使用std::string时可能是可以的;但是如果您想使用std::basic_string<char16_t>,则无法使用std::getline。这里是另一种可能的实现方式:

template <class CharT> bool tokenizestring(const std::basic_string<CharT> &input, CharT separator, typename std::basic_string<CharT>::size_type &pos, std::basic_string<CharT> &token) {
    if (pos >= input.length()) {
        // if input is empty, or ends with a separator, return an empty token when the end has been reached (and return an out-of-bound position so subsequent call won't do it again)
        if ((pos == 0) || ((pos > 0) && (pos == input.length()) && (input[pos-1] == separator))) {
            token.clear();
            pos=input.length()+1;
            return true;
        }
        return false;
    }
    typename std::basic_string<CharT>::size_type separatorPos=input.find(separator, pos);
    if (separatorPos == std::basic_string<CharT>::npos) {
        token=input.substr(pos, input.length()-pos);
        pos=input.length();
    } else {
        token=input.substr(pos, separatorPos-pos);
        pos=separatorPos+1;
    }
    return true;
}

然后像这样使用它:

std::basic_string<char16_t> s;
std::basic_string<char16_t> token;
std::basic_string<char16_t>::size_type tokenPos=0;
while (tokenizestring(s, (char16_t)' ', tokenPos, token)) {
    ...
}

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