【问题标题】:Using strtok with a std::string将 strtok 与 std::string 一起使用
【发布时间】:2010-09-22 07:21:18
【问题描述】:

我有一个要标记的字符串。 但是 C strtok() 函数要求我的字符串是 char*。 我怎样才能简单地做到这一点?

我试过了:

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

失败是因为它变成了const char*,而不是char*

【问题讨论】:

标签: c++ strtok


【解决方案1】:

Chris 的答案在使用 std::string 时可能没问题;但是,如果您想使用 std::basic_string,则不能使用 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)) {
    ...
}

【讨论】:

    【解决方案2】:

    类型转换为 (char*) 让它为我工作!

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

    【讨论】:

    • 这行不通。 strtok 将修改 str 的内部。我想这是用户不想要的副作用。解决方案是创建一个 char 缓冲区并首先将 str 字符串复制到缓冲区中。
    【解决方案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;
    }
    

    输出

    流行音乐
    多普
    罗普

    【讨论】:

    • 注意:原来的std::string 将不再具有相同的值,因为 strtok 将找到的分隔符替换为空终止符,而不是返回字符串的副本。如果要保留原始字符串,请创建字符串的副本并将其传递给 strtok。
    • @user233009 注意:如果strtok 仅处理单个分隔符,则可以通过在每次迭代中放回分隔符替换空终止符来保留字符串的原始值。
    【解决方案4】:

    失败是因为 str.c_str() 返回常量字符串,但 char * strtok (char * str, const char * delimiters ) 需要易失字符串。因此,您需要使用 *const_cast> 使其具有可变性。 我给你一个完整但很小的程序来使用 C strtok() 函数标记字符串。

       #include <iostream>
       #include <string>
       #include <string.h> 
       using namespace std;
       int main() {
           string s="20#6 5, 3";
           // strtok requires volatile string as it modifies the supplied string in order to tokenize it 
           char *str=const_cast< char *>(s.c_str());    
           char *tok;
           tok=strtok(str, "#, " );     
           int arr[4], i=0;    
           while(tok!=NULL){
               arr[i++]=stoi(tok);
               tok=strtok(NULL, "#, " );
           }     
           for(int i=0; i<4; i++) cout<<arr[i]<<endl;
    
    
           return 0;
       }
    

    注意: strtok 可能并不适合所有情况,因为传递给函数的字符串会被分解为更小的字符串。请ref 更好地了解 strtok 功能。

    strtok 的工作原理

    添加了一些打印语句,以更好地了解每次调用 strtok 时字符串发生的变化以及它如何返回令牌。

    #include <iostream>
    #include <string>
    #include <string.h> 
    using namespace std;
    int main() {
        string s="20#6 5, 3";
        char *str=const_cast< char *>(s.c_str());    
        char *tok;
        cout<<"string: "<<s<<endl;
        tok=strtok(str, "#, " );     
        cout<<"String: "<<s<<"\tToken: "<<tok<<endl;   
        while(tok!=NULL){
            tok=strtok(NULL, "#, " );
            cout<<"String: "<<s<<"\t\tToken: "<<tok<<endl;
        }
        return 0;
    }
    

    输出:

    string: 20#6 5, 3
    
    String: 206 5, 3    Token: 20
    String: 2065, 3     Token: 6
    String: 2065 3      Token: 5
    String: 2065 3      Token: 3
    String: 2065 3      Token: 
    

    strtok 遍历字符串第一次调用找到非删除字符(在本例中为 2)并将其标记为标记 start 然后继续扫描分隔符并将其替换为空字符(# 被替换在实际字符串中)并返回指向令牌开始字符的start(即,它返回以null终止的令牌20)。在随后的调用中,它从下一个字符开始扫描,如果发现则返回令牌,否则返回 null。随后它返回令牌 6、5、3。

    【讨论】:

    • 仅供参考:strtok 将改变 s 的值。你不应该使用 const_cast,因为这只是隐藏了一个问题。
    • 这会通过使用c_str() 的结果来修改字符串导致未定义的行为
    • @M.M 添加了 strtok 函数的更多说明和工作。希望它能帮助人们了解何时使用它
    【解决方案5】:
    #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 我相信您可以使用 regex_token_iterator 使用多个分隔符进行标记。感谢过去的爆炸,我很久以前就回答了原来的问题:)
    【解决方案6】:

    如果您不介意开源,您可以使用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(' ');
    

    【讨论】:

      【解决方案7】:

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

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

      此时许多优秀的人会跳起来对着屏幕大喊大叫。但这就是事实。大约 2 年前

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

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

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

      但这是不正确的。实际上,该函数用 \0 替换了 first 出现的分隔符。字符串的大小没有变化。如果我们有这个字符串:

      一二三四

      我们最终会得到

      一\0二\0--三\0-四

      所以我的解决方法很简单:

      
      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 );
      }
      

      阅读http://www.archivum.info/comp.lang.c++/2008-05/02889/does_std::string_have_something_like_CString::GetBuffer上的讨论

      【讨论】:

      • -1。 strtok() 适用于以空字符结尾的字符串,而std::string 的缓冲区不需要以空字符结尾。没有办法绕过c_str()
      • @SnakE std::string 的缓冲区 需要以空值终止。 datac_str 必须相同,data() + i == &amp;operator[](i) for every i in [0, size()]
      • @Leushenko 你说对了一部分。自 C++11 起才保证空终止。我在答案中添加了注释。一旦我的编辑被接受,我会立即取消我的 -1。
      • 这个 hack 不值得。这个“优雅”的解决方案以几种方式破坏了 std::string 对象。 std::cout &lt;&lt; str &lt;&lt; " " &lt;&lt; str.size(); std::cout &lt;&lt; str.c_str()&lt;&lt; " " &lt;&lt; strlen(str.c_str()); 之前:some-text-to-split 18 some-text-to-split 18 之后:sometexttosplit 18 some 4.
      • 上面代码中“token = strtok(NULL, seps)”的用途是什么。请回答因为试图搜索这个用途,但没有得到太多。
      【解决方案8】:

      编辑: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, @Sherm Pendley :您阅读了结论还是只阅读了代码 sn-p ?我编辑了我的答案以澄清我想在这里展示的内容。 Rgds。
      • @Philippe - 是的,我只阅读了代码。很多人会这样做,直接进入代码并跳过解释。也许将解释作为注释放在代码中会是一个好主意吗?无论如何,我删除了我的反对票。
      • 有人知道编译器(警告开关)或静态代码分析器会警告此类问题吗?
      【解决方案9】:

      首先我会说使用 boost tokenizer。
      或者,如果您的数据是空格分隔的,那么字符串流库非常有用。

      但是以上两个都已经涵盖了。
      因此,作为第三种 C-Like 替代方案,我建议将 std::string 复制到缓冲区中进行修改。

      std::string   data("The data I want to tokenize");
      
      // Create a buffer of the correct length:
      std::vector<char>  buffer(data.size()+1);
      
      // copy the string into the buffer
      strcpy(&buffer[0],data.c_str());
      
      // Tokenize
      strtok(&buffer[0]," ");
      

      【讨论】:

        【解决方案10】:
        1. 如果boost 在您的系统上可用(我认为它是当今大多数 Linux 发行版的标准),它有一个您可以使用的 Tokenizer 类。

        2. 如果没有,那么快速的 Google 会为 std::string 找到 hand-rolled tokenizer,您可能只需复制和粘贴即可。很短。

        3. 而且,如果您不喜欢其中任何一个,那么这是我编写的一个 split() 函数,以使我的生活更轻松。它将使用“delim”中的任何字符作为分隔符将字符串分成几部分。碎片被附加到“零件”向量中:

          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));
              }
            }
          }
          

        【讨论】:

        • 手卷链接坏了
        【解决方案11】:

        假设您所说的“字符串”是指 C++ 中的 std::string,您可能会查看 Boost 中的 Tokenizer 包。

        【讨论】:

          【解决方案12】:

          我想语言是 C 或 C++...

          strtok,IIRC,用 \0 替换分隔符。这就是它不能使用 const 字符串的原因。 为了“快速”解决这个问题,如果字符串不是很大,你可以 strdup() 它。如果您需要保持字符串不变(const 建议...),这是明智的。

          另一方面,您可能想要使用另一个标记器,也许是手卷的,在给定的参数上不那么暴力。

          【讨论】:

            【解决方案13】:

            复制字符串,对其进行标记,然后释放它。

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

            【讨论】:

            • 这不是更好的问题吗,当所讨论的语言有更好的原生选项时,为什么还要使用 strtok?
            • 不一定。如果问题的上下文围绕维护脆弱的代码库,那么放弃现有方法(在我的示例中名义上是 strtok)比改变方法风险更大。如果问题没有更多上下文,我更愿意回答所问的内容。
            • 如果提问者是新手,你应该在使用令牌之前不要做 free()... :-)
            • 我怀疑使用更强大的本机标记器比插入调用库的新代码更安全,该库将空值插入传递给它的内存块......这就是为什么我不认为按要求回答问题是个好主意。
            • 请注意,strtok() 不是线程安全的或可重入的。在一个有多个任务的程序中,应该避免它。
            猜你喜欢
            • 2015-09-15
            • 1970-01-01
            • 2016-07-30
            • 1970-01-01
            • 2010-12-23
            • 2018-03-22
            • 2020-12-05
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多