问题

C++ 的 string 为什么不提供 split 函数?

回答
在 C++ 标准库的 `std::string` 类设计之初,确实没有提供一个直接的 `split` 函数。这与其他一些高级语言(如 Python、Java)中普遍存在的 `split` 方法有所不同。要理解为什么会这样,我们需要深入探究 C++ 的设计哲学、标准库的演进过程以及当时的开发环境和需求。

1. C++ 的设计哲学:面向低层控制与效率

C++ 的核心优势在于它提供了对底层内存的精细控制和极高的执行效率。它不像 Python 或 Java 那样将许多高级抽象封装在语言的核心层面,而是倾向于提供基础的构建块,让开发者可以根据具体需求自由组合和实现。

字符串的本质与历史: 在 C++ 的早期,字符串处理主要依赖于 C 风格的字符数组(`char`)。`std::string` 的出现是对 C 风格字符串的一种面向对象封装,提供了更安全、更方便的内存管理和操作。然而,即便如此,`std::string` 的设计也继承了一部分 C 的思维模式——即对数据进行直接操作的倾向。
低层操作的优先: 当 C++ 标准库的设计者考虑 `split` 功能时,他们可能认为,一个通用的 `split` 函数的实现方式会多种多样,并且其效率和具体行为(例如,如何处理连续的界定符、是否保留空字符串等)会受到具体场景的影响。与其提供一个可能不满足所有用户需求的通用函数,不如提供更底层的工具,让开发者可以根据自己的需要来构建 `split` 逻辑。

2. 标准库的演进与审慎的引入

C++ 标准库的设计是一个漫长且审慎的过程。每次新功能的引入都经过了仔细的讨论和权衡。

现有工具的可用性: 在没有直接 `split` 函数的情况下,开发者可以通过多种方式实现字符串分割:
`find` 和 `substr` 组合: 这是最常见和基础的方法。使用 `std::string::find` 找到界定符的位置,然后使用 `std::string::substr` 提取子字符串。这种方法虽然需要手动编写循环和边界检查,但提供了极大的灵活性。
`std::stringstream`: 可以将字符串读入流,然后使用 `std::getline` 以特定界定符读取。这是一种相对简洁的方式,特别适合用单个字符作为界定符的场景。
迭代器: 利用字符串的迭代器配合查找算法,也可以实现更精细化的分割。
性能考虑: 一个通用的 `split` 函数可能需要在内部进行多次查找、复制和内存分配。如果界的定义非常复杂(例如,需要匹配正则表达式),那么实现一个高效且通用的 `split` 函数会变得相当困难。与其引入一个潜在效率不佳的通用函数,不如让开发者自行优化特定场景下的分割逻辑。
版本迭代与需求变化: 随着 C++ 标准的不断更新(C++11, C++17, C++20 等),标准库也在不断吸收新的需求和更优的设计。虽然 `std::string` 本身没有直接的 `split`,但其他 C++ 标准库组件的进步,以及社区提供的各种库(如 Boost),都为字符串处理提供了更丰富的选择。

3. 社区的解决方案与第三方库的丰富性

在 C++ 社区中,开发者们早已开发了各种自定义的 `split` 函数,并在各种开源项目和库中得到广泛应用。

Boost.StringAlgo: Boost 库(一个事实上的 C++ 标准扩展库)的 `Boost.StringAlgo` 模块提供了非常强大且灵活的字符串处理工具,其中就包含了多种 `split` 的实现,能够满足各种复杂的分割需求。很多时候,C++ 标准库的设计也会参考 Boost 的成功经验。
自定义函数随处可见: 在各种编程指南、博客和论坛中,都可以找到 C++ 开发者分享的 `split` 函数实现。这些实现往往针对特定需求进行了优化,例如:
区分对待连续的界定符(是否生成空字符串)。
支持多个界定符的分割。
处理开头的界定符。
返回 `std::vector`、`std::list` 等不同容器类型。

4. 对比其他语言的考量

为什么 Python 或 Java 提供了 `split`?

语言设计目标不同: Python 和 Java 的设计哲学更倾向于提供高级的抽象和便捷的开发体验。它们的标准库通常包含了更多“开箱即用”的功能,以减少开发者的样板代码。字符串分割是一个非常常见且基础的操作,直接提供 `split` 函数可以极大地提高开发效率。
内存管理模型: 在这些语言中,内存管理通常是自动的,开发者无需直接关注对象的创建和销毁。因此,`split` 函数内部的字符串创建和复制对性能的影响不如 C++ 中那样敏感。

总结

`std::string` 没有直接提供 `split` 函数,并非是设计上的遗漏,而是 C++ 设计哲学、标准库演进过程以及对底层控制与效率的追求的综合体现。它鼓励开发者根据具体需求,利用现有的基础工具(如 `find`, `substr`, `stringstream`)来构建自己的字符串分割逻辑,或者依赖于社区成熟的第三方库(如 Boost)。这种方式虽然可能需要开发者编写更多的代码,但也赋予了极大的灵活性和对性能的精细控制能力,这正是 C++ 的魅力所在。随着 C++ 标准的不断发展,未来标准库是否会引入更高级的字符串操作,也是一个值得期待的趋势。

网友意见

user avatar

2019-10-22 更新:感谢 @依云 在评论区提到了 Lazy Evaluation,这个特性在未来 C++ Coroutine 加入标准后将很容易、很优雅地实现。但据我所知,很多第三方库(不限于 C++ 语言)都提供了两套 API,支持实时计算和 Lazy Evaluation。我认为即使确实存在 Lazy Evaluation 可能在一定程度上提升性能的场景,实时计算也是存在充足的需求的,至少它省去了维护 Coroutine 相关数据结构的开销。


我猜应该很多人都手写过 split 吧,我也曾经在自己的项目中写过类似这样的函数。虽然我没有深入研究过 std::string 的标准,但根据我个人的经验,std::string 没有提供 split 成员函数可能有以下原因:

  1. 功能上,我发现很多第三方库(不限于 C++ 语言)都提供一些其他的特性,比如对正则的支持、对键值对的支持、选择性删除首尾空格、忽略空串(这个需求貌似不是很大) 等等。作为 C++ 标准,我们应该支持上述哪些特性、或者一个都不支持呢?
  2. 兼容性上,C++ 语言不仅有 std::basic_string,还有 std::basic_string_view,甚至 C++20 之后还可能加入一些 Concepts(我不确定),所以仅把 split 作为 std::basic_string 的成员函数有失偏颇。不仅被分割的字符串可能有各种各样的类型,分割符、保存结果的容器在用户手里都可能具有不同的类型。即使不支持第一点中所述任一功能,仅支持这些不同类型的语义就会十分复杂。
  3. 其他答案中也有提到,很多 C++ 第三方库都强依赖于 std::vector;这样对于大部分用户来说虽然很难产生性能问题,但对于 STL 来说这种强依赖是不能接受的。


不过即使上述问题客观存在,我相信办法总比问题多。即使 split 在短时间内可能很难加入标准,我们也可以设计出符合我们需求的 split 函数。


如果大家感兴趣可以参考下我之前项目中使用的版本(我自己手写的),不支持第一点中的那些特性,但基本解决第 2、3 点问题,算是抛砖引玉,欢迎大家吐槽。


首先是函数签名:

       template <class Str, class Spt, class C = std::vector</* see below */>> C split(const Str& str, const Spt& splitter, C result = C{});      

模板类型说明:

Str: 待分割字符串类型,可以是 std::basic_stringstd::string_view、C-style 字符串或其他具有 findsubstr 的自定义类型。

Spt: 分割符,可以是 Str 的任何类型或字符类型。

C: 结果容器,默认为 std::vector 实例,支持定制。


实现(C++17):

       #include <type_traits> #include <string> #include <string_view> #include <vector>  namespace detail {  template <class SFINAE, class Str> struct sfinae_is_general_string : std::false_type {};  template <class Str> struct sfinae_is_general_string<std::enable_if_t<std::is_convertible_v<     decltype(std::declval<Str>().length()), std::size_t>>, Str>     : std::true_type {};  template <class Str> decltype(auto) normalize_str(const Str& s) {   if constexpr (std::is_pointer_v<Str>) {     return std::basic_string<std::remove_pointer_t<Str>>{s};   } else if constexpr (std::is_array_v<Str>) {     return std::basic_string<std::remove_extent_t<Str>>{s};   } else {     return s;   } }  template <class Spt> decltype(auto) normalize_splitter(const Spt& s) {   if constexpr (std::is_pointer_v<Spt>) {     return std::basic_string_view<std::remove_pointer_t<Spt>>{s};   } else if constexpr (std::is_array_v<Spt>) {     return std::basic_string_view<std::remove_extent_t<Spt>>{s};   } else if constexpr (sfinae_is_general_string<void, Spt>::value) {     return s;   } else {     return std::basic_string<Spt>(1u, s);   } }  template <class Str> using normalized_str_t = std::decay_t<std::invoke_result_t<     decltype(normalize_str<Str>), Str>>;  template <class Str, class Spt, class Cp> void do_split(const Str& str, const Spt& splitter, Cp* result) {   std::size_t substr_begin = 0u, substr_end;   while ((substr_end = str.find(splitter, substr_begin)) != Str::npos) {     result->push_back(str.substr(substr_begin, substr_end));     substr_begin = substr_end + splitter.length();   }   result->push_back(str.substr(substr_begin, str.size())); }  }  // namespace detail  template <class Str, class Spt,     class C = std::vector<detail::normalized_str_t<Str>>> C split(const Str& str, const Spt& splitter, C result = C{}) {   detail::do_split(detail::normalize_str(str),       detail::normalize_splitter(splitter), &result);   return result; }      
user avatar

来自Stack Overflow

For what it's worth, here's another way to extract tokens from an input string, relying only on standard library facilities. It's an example of the power and elegance behind the design of the STL.

         #include <iostream> #include <string> #include <sstream> #include <algorithm> #include <iterator>  int main() {     using namespace std;     string sentence = "And I feel fine...";     istringstream iss(sentence);     copy(istream_iterator<string>(iss),          istream_iterator<string>(),          ostream_iterator<string>(cout, "
")); }       

Instead of copying the extracted tokens to an output stream, one could insert them into a container, using the same generic copy algorithm.

         vector<string> tokens; copy(istream_iterator<string>(iss),      istream_iterator<string>(),      back_inserter(tokens));       

... or create the vector directly:

         vector<string> tokens{istream_iterator<string>{iss},                       istream_iterator<string>{}};        
Split a string in C++?
user avatar

C++11以前有很多原因不能提供一个通用的split,比如说需要考虑split以后的结果存储在什么类型的容器中,可以是vector、list等等包括自定义容器,很难提供一个通用的;再比如说需要split的源字符串很大的时候运算的时间可能会很长,所以这个split最好是lazy的,每次只返回一条结果。

C++11之前只能自己写,我目前发现的史上最优雅的一个实现是这样的:

       void split(const string& s, vector<string>& tokens, const string& delimiters = " ") {     string::size_type lastPos = s.find_first_not_of(delimiters, 0);     string::size_type pos = s.find_first_of(delimiters, lastPos);     while (string::npos != pos || string::npos != lastPos) {         tokens.push_back(s.substr(lastPos, pos - lastPos));//use emplace_back after C++11         lastPos = s.find_first_not_of(delimiters, pos);         pos = s.find_first_of(delimiters, lastPos);     } }      

从C++11开始,标准库中提供了regex,regex用来做split就是小儿科了,比如:

       std::string text = "Quick brown fox."; std::regex ws_re("\s+"); // whitespace std::vector<std::string> v(std::sregex_token_iterator(text.begin(), text.end(), ws_re, -1),      std::sregex_token_iterator()); for(auto&& s: v)     std::cout<<s<<"
";      

C++17提供的string_view可以加速上面提到的第一个split实现,减少拷贝,性能有不小提升,参看此文:Speeding Up string_view String Split Implementation

从C++20开始,标准库中提供了ranges,有专门的split view,只要写str | split(' ')就可以切分字符串,如果要将结果搜集到vector<string>中,可以这样用(随手写的,可能不是最简):

       string str("hello world test split"); auto sv = str     | ranges::views::split(' ')      | ranges::views::transform([](auto&& i){         return i | ranges::to<string>(); })      | ranges::to<vector>();      for(auto&& s: sv) {     cout<<s<<"
"; }      

其实C语言里面也有一个函数strtok用于char*的split,例如:

       #include <string.h> #include <iostream> #include <string> using namespace std; int main()  {     string str = "one two three four five";     char *token = strtok(str.data(), " ");// non-const data() needs c++17     while (token != NULL) {         std::cout << token << '
';         token = strtok(NULL, " ");     } }  //如你所愿,输出如下: one two three four five      

这里要注意的是strtok的第一个参数类型是char*而不是const char*,实际上strtok的确会改变输入的字符串。

参考文献:

史上最优雅的split来自这里

ranges for C++20的用法参考我写的教程

欢迎关注个人公众号「树屋编程」,专注分享C++的学习知识与程序员的职场体验

类似的话题

  • 回答
    在 C++ 标准库的 `std::string` 类设计之初,确实没有提供一个直接的 `split` 函数。这与其他一些高级语言(如 Python、Java)中普遍存在的 `split` 方法有所不同。要理解为什么会这样,我们需要深入探究 C++ 的设计哲学、标准库的演进过程以及当时的开发环境和需求.............
  • 回答
    您提出的问题非常棒,触及了 C++ 社区中一个长期存在且略带争议的话题:为什么那么多 C++ 开源库选择自己实现或包装 `std::string`,而不是直接使用标准库提供的 `std::string`?首先,我们需要明确一点:并非“大多数” C++ 开源库都选择“自己实现 string”。 这是一.............
  • 回答
    判断一个字符串是否能成为 C 合法的变量名,其实就是遵循 C 语言的命名规则。这些规则是为了让代码清晰、易读,并且能够被编译器正确解析。不像我们在现实生活中起名字,随便什么组合都可以,编程语言有它自己的“户口本”,名字得在这个户口本上登记上才行。咱们就一点点捋清楚,一个字符串要成为 C 变量名的“入.............
  • 回答
    在C中,`String.Format()` 方法提供了两种主要的字符串格式化方式,一种是使用索引占位符,另一种是命名占位符。理解它们之间的区别以及各自的适用场景,可以帮助你写出更清晰、更易维护的代码。1. 使用索引占位符的 `String.Format()`这种方式的占位符以大括号 `{}` 包裹,.............
  • 回答
    在 C 里,当你直接写 `string + int` 这样的操作时,背后实际上发生了一系列的事情,而不是简单的“拼接”。我们来详细拆解一下这个过程,尽量避免那些空泛的、AI 惯用的表述。首先,要明白 C 中的 `string` 类型是什么。`string` 在 C 中是一个引用类型,更具体地说,它是.............
  • 回答
    C++ 模板:功能强大的工具还是荒谬拙劣的小伎俩?C++ 模板无疑是 C++ 语言中最具争议但也最引人注目的一项特性。它既能被誉为“代码生成器”、“通用编程”的基石,又可能被指责为“编译时地狱”、“难以理解”的“魔法”。究竟 C++ 模板是功能强大的工具,还是荒谬拙劣的小伎俩?这需要我们深入剖析它的.............
  • 回答
    C++ 是一门强大而灵活的编程语言,它继承了 C 语言的高效和底层控制能力,同时引入了面向对象、泛型编程等高级特性,使其在各种领域都得到了广泛应用。下面我将尽可能详细地阐述 C++ 的主要优势: C++ 的核心优势:1. 高性能和底层控制能力 (Performance and LowLevel C.............
  • 回答
    C++ 的核心以及“精通”的程度,这是一个非常值得深入探讨的话题。让我尽量详细地为您解答。 C++ 的核心究竟是什么?C++ 的核心是一个多层次的概念,可以从不同的角度来理解。我将尝试从以下几个方面来阐述:1. 语言设计的哲学与目标: C 的超集与面向对象扩展: C++ 最初的目标是成为 C 语.............
  • 回答
    C++ 和 Java 都是非常流行且强大的编程语言,它们各有优劣,并在不同的领域发挥着重要作用。虽然 Java 在很多方面都非常出色,并且在某些领域已经取代了 C++,但仍然有一些 C++ 的独特之处是 Java 无法完全取代的,或者说取代的成本非常高。以下是 C++ 的一些 Java 不能(或难以.............
  • 回答
    C++ `new` 操作符与 `malloc`:底层联系与内存管理奥秘在C++中,`new` 操作符是用于动态分配内存和调用构造函数的关键机制。许多开发者会好奇 `new` 操作符的底层实现,以及它与C语言中的 `malloc` 函数之间的关系。同时,在对象生命周期结束时,`delete` 操作符是.............
  • 回答
    好,咱们来聊聊 C++ 单例模式里那个“为什么要实例化一个对象,而不是直接把所有成员都 `static`”的疑问。这确实是很多初学者都会纠结的地方,感觉直接用 `static` 更省事。但这里面涉及到 C++ 的一些核心概念和设计上的考量,咱们一点点掰开了说。 先明确一下单例模式的目标在深入“`st.............
  • 回答
    C 扩展方法:一把双刃剑C 的扩展方法,顾名思义,允许我们为现有的类型添加新的方法,而无需修改原始类型的源代码。这种能力最初听起来像是魔法,能够让代码更加优雅、富有表现力,并且提升了代码的复用性。然而,正如许多强大的工具一样,扩展方法也是一把双刃剑,如果使用不当,可能会导致代码可读性下降、维护困难,.............
  • 回答
    C++ 的 `std::list`,作为 STL(Standard Template Library)中的一员,它是一种双向链表(doubly linked list)。它的核心特点在于,每个节点都存储了数据本身,以及指向前一个节点和后一个节点的指针。这使得 `std::list` 在某些特定场景下.............
  • 回答
    你问了一个非常关键的问题,而且问得非常实在。确实,C++ 的智能指针,尤其是 `std::unique_ptr` 和 `std::shared_ptr`,在很大程度上解决了 C++ 中常见的野指针和内存泄漏问题。这玩意儿在 C++ 世界里,堪称“救世主”般的存在。那么,为什么大家对 Rust 的内存.............
  • 回答
    C++ 中的常量后缀,顾名思义,就是用来标识字面量(literal)是何种类型的。虽然编译器通常能够通过字面量的形式推断出其类型,但在很多情况下,使用常量后缀能够明确表达开发者的意图,避免潜在的类型转换问题,并提升代码的可读性和健壮性。我们来详细探讨一下常量后缀在哪些情况下特别有用,并说明其背后的原.............
  • 回答
    CRTP,也就是Curiously Recurring Template Pattern(奇特的递归模板模式),在C++中,它是一种利用模板的静态分派特性来实现多态的一种精巧技巧。很多人听到“多态”首先想到的是虚函数和运行时多态,但CRTP带来的多态是“静态多态”,这意味着多态的决策是在编译期完成的.............
  • 回答
    C++ 运行时多态:性能的代价与权衡在 C++ 的世界里,我们常常惊叹于它的灵活性和表达力。其中,运行时多态(Runtime Polymorphism)是实现这一能力的关键机制之一,它允许我们在程序运行时根据对象的实际类型来决定调用哪个函数。这就像一个剧团的导演,在舞台上,他可以根据演员扮演的角色,.............
  • 回答
    C++的move构造,作为语言引入的一项重要特性,其设计初衷是为了解决资源管理中的性能瓶颈,特别是针对那些拥有昂贵资源(如堆内存、文件句柄、网络连接等)的对象。它允许我们将一个对象的资源“转移”到另一个对象,而不是通过昂贵的拷贝操作来复制这些资源。然而,随着这项特性的应用和深入理解,关于其设计是否“.............
  • 回答
    sizeof 关键字在 C++ 中,并不是一个普通的函数,而是一个编译时常量。理解它的实现,关键在于区分它在编译期和运行时的行为。1. 编译期的魔法:类型的大小计算当你使用 `sizeof` 关键字时,比如 `sizeof(int)` 或者 `sizeof(MyClass)`,编译器会立即在编译阶段.............
  • 回答
    C++ 的 `switch` 语句之所以不默认添加 `break` 语句,这是 C++ 设计者们经过深思熟虑后做出的一个选择,其背后有明确的理由和意图。理解这一点,需要我们深入到 `switch` 语句的本质和它与其他控制流语句的区别。 1. fallthrough(贯穿)的意图与灵活性C++ 的 .............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有