如何截取一个std::string?

1001
我目前在我的程序中使用以下代码来去除所有的std::string的右空格:
std::string s;
s.erase(s.find_last_not_of(" \n\r\t")+1);

它运行良好,但我想知道是否存在一些极端情况会导致它失败?
当然,欢迎提供优雅的替代方案和左修剪解决方案。
52个回答

849
自c++17开始,标准库的某些部分已被删除。幸运的是,从c++11开始,我们有了lambda表达式,这是一种更优秀的解决方案。
#include <algorithm> 
#include <cctype>
#include <locale>

// trim from start (in place)
static inline void ltrim(std::string &s) {
    s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
        return !std::isspace(ch);
    }));
}

// trim from end (in place)
static inline void rtrim(std::string &s) {
    s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
        return !std::isspace(ch);
    }).base(), s.end());
}

// trim from both ends (in place)
static inline void trim(std::string &s) {
    rtrim(s);
    ltrim(s);
}

// trim from start (copying)
static inline std::string ltrim_copy(std::string s) {
    ltrim(s);
    return s;
}

// trim from end (copying)
static inline std::string rtrim_copy(std::string s) {
    rtrim(s);
    return s;
}

// trim from both ends (copying)
static inline std::string trim_copy(std::string s) {
    trim(s);
    return s;
}

感谢https://dev59.com/SlcP5IYBdhLWcg3wAmEE#44973498提出现代解决方案。

原始答案:

我倾向于使用以下三种方法来满足我的修剪需求:

#include <algorithm> 
#include <functional> 
#include <cctype>
#include <locale>

// trim from start
static inline std::string &ltrim(std::string &s) {
    s.erase(s.begin(), std::find_if(s.begin(), s.end(),
            std::not1(std::ptr_fun<int, int>(std::isspace))));
    return s;
}

// trim from end
static inline std::string &rtrim(std::string &s) {
    s.erase(std::find_if(s.rbegin(), s.rend(),
            std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end());
    return s;
}

// trim from both ends
static inline std::string &trim(std::string &s) {
    return ltrim(rtrim(s));
}

它们相当易于理解,并且工作得非常好。

编辑:顺便提一下,我在这里使用了std::ptr_fun来帮助消除std::isspace的歧义,因为实际上有第二个支持本地化的定义。这也可以是一个转换,但我倾向于更喜欢这种方式。

编辑:针对一些评论关于通过引用接受参数、修改并返回它的问题。我同意。我可能更喜欢的实现方式是两组函数,一组用于原地操作,另一组则创建副本。更好的示例集将是:

#include <algorithm> 
#include <functional> 
#include <cctype>
#include <locale>

// trim from start (in place)
static inline void ltrim(std::string &s) {
    s.erase(s.begin(), std::find_if(s.begin(), s.end(),
            std::not1(std::ptr_fun<int, int>(std::isspace))));
}

// trim from end (in place)
static inline void rtrim(std::string &s) {
    s.erase(std::find_if(s.rbegin(), s.rend(),
            std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end());
}

// trim from both ends (in place)
static inline void trim(std::string &s) {
    rtrim(s);
    ltrim(s);
}

// trim from start (copying)
static inline std::string ltrim_copy(std::string s) {
    ltrim(s);
    return s;
}

// trim from end (copying)
static inline std::string rtrim_copy(std::string s) {
    rtrim(s);
    return s;
}

// trim from both ends (copying)
static inline std::string trim_copy(std::string s) {
    trim(s);
    return s;
}

为了保留上下文和让最受欢迎的答案仍然可用,我将保留原始答案。


37
这段代码在一些国际字符串(在我的情况下是shift-jis编码,存储在std::string中)上出现了故障;我最终使用了boost::trim来解决这个问题。 - Tom
5
我会使用指针而不是引用,这样从调用点更容易理解这些函数会直接在原字符串上进行修改,而不是创建一个副本。 - Marco Leogrande
4
请注意,使用 isspace 函数时,非 ASCII 字符可能会导致未定义的行为。http://stacked-crooked.com/view?id=49bf8b0759f0dd36dffdad47663ac69f - R. Martinho Fernandes
12
为什么会出现静态变量?这种情况是否适合使用匿名命名空间? - Trevor Hickey
3
@TrevorHickey,如果您喜欢的话,您可以使用匿名命名空间。 - Evan Teran
显示剩余21条评论

460

使用Boost字符串算法是最简单的方法:

#include <boost/algorithm/string.hpp>

std::string str("hello world! ");
boost::trim_right(str);

str现在是"hello world!"。还有trim_lefttrim,它们会修剪字符串的两侧。

如果您将_copy后缀添加到上述任何函数名称中,例如trim_copy,函数将返回已修剪的字符串副本,而不是通过引用修改它。

如果您将_if后缀添加到上述任何函数名称中,例如trim_copy_if,则可以修剪满足您自定义谓词的所有字符,而不仅仅是空格。


1
Boost使用什么来确定一个字符是否为空格? - Tom
8
这取决于所在区域。我的默认区域设置(VS2005,英文)表示制表符、空格、回车、换行符、垂直制表符和换页符都会被修剪掉。 - MattyT
6
我已经使用了很多 Boost 库,#include <boost/format.hpp> <boost/tokenizer.hpp> <boost/lexical_cast.hpp>,但是考虑到已经有基于 std::string::erase 的替代方案,我担心添加 <boost/algorithm/string.hpp> 会导致代码膨胀。不过很高兴地报告,在添加 Boost 的 trim 函数之前和之后比较 MinSizeRel 构建时,并没有增加我的代码大小(可能已经在其他地方使用了),而且我的代码也不会因为额外的一些函数而变得混乱。 - Rian Sanderson
@MattyT:你是用什么参考资料来确定这个列表中的字符是否为空格? - Faheem Mitha
2
并没有真正回答问题,问题要求使用std::string(而不是boost或任何其他库...) - hfrmobile
显示剩余2条评论

101

你所做的很好,也很可靠。我用了相同的方法已经有很长时间了,还没有找到一个更快的方法:

const char* ws = " \t\n\r\f\v";

// trim from end of string (right)
inline std::string& rtrim(std::string& s, const char* t = ws)
{
    s.erase(s.find_last_not_of(t) + 1);
    return s;
}

// trim from beginning of string (left)
inline std::string& ltrim(std::string& s, const char* t = ws)
{
    s.erase(0, s.find_first_not_of(t));
    return s;
}

// trim from both ends of string (right then left)
inline std::string& trim(std::string& s, const char* t = ws)
{
    return ltrim(rtrim(s, t), t);
}

通过提供要删除的字符,您可以灵活地删除非空格字符,并且只删除您想要删除的字符,这样可以提高效率。

如果您在CharT上使用basic_string和template,就可以对所有字符串执行此操作,只需为空格使用模板变量,以便像ws <CharT>一样使用它。从技术上讲,在那一点上,您可以将其准备好c++20,并将其标记为constexpr,因为这意味着内联。 - Beached
确实如此。但在这里回答有点复杂。我已经为此编写了模板函数,它肯定相当复杂。我尝试了许多不同的方法,仍然不确定哪种方法是最好的。 - Galik

71

试试这个,它对我有效。

inline std::string trim(std::string& str)
{
    str.erase(str.find_last_not_of(' ')+1);         //suffixing spaces
    str.erase(0, str.find_first_not_of(' '));       //prefixing spaces
    return str;
}

4
str.find_last_not_of(x)函数返回第一个不等于x的字符位置。只有当没有任何字符与x不匹配时,它才会返回npos。在这个例子中,如果没有后缀空格,它将返回相当于str.length() - 1,从而实际上是执行了str.erase((str.length() - 1) + 1)。除非我完全错了。 - Travis
8
为避免不必要地调用复制构造函数,应该返回std::string&。 - heksesang
14
我不明白为什么在修改返回参数后,它会返回一个副本? - Galik
3
为什么要返回副本而不是引用让我感到困惑。对我来说,返回std::string&更有意义。 - Galik
2
如果您更改顺序(先删除后缀空格,然后再添加前缀空格),它将更有效率。 - CITBL
显示剩余6条评论

67

使用以下代码可以从 std::strings 中右侧裁剪(尾随)空格和制表符 (ideone):

// trim trailing spaces
size_t endpos = str.find_last_not_of(" \t");
size_t startpos = str.find_first_not_of(" \t");
if( std::string::npos != endpos )
{
    str = str.substr( 0, endpos+1 );
    str = str.substr( startpos );
}
else {
    str.erase(std::remove(std::begin(str), std::end(str), ' '), std::end(str));
}

为了平衡一下,我也会包括左侧修剪代码 (ideone):

// trim leading spaces
size_t startpos = str.find_first_not_of(" \t");
if( string::npos != startpos )
{
    str = str.substr( startpos );
}

5
这不会检测到其他形式的空白符... 特别是换行符、回车符等。 - Tom
5
使用str.substr(...).swap(str)更好,可以节省一次赋值操作。 - updogliu
4
会不会使用移动赋值运算符 basic_string& operator= (basic_string&& str) noexcept; - nurettin
9
此答案不会更改所有为空格的字符串。这是一个失败。 - Tom Andersen
1
为什么不使用简单的resize()来进行右侧修剪呢?它可能只涉及一个整数减少操作,这样就不会更便宜了... - Lightness Races in Orbit
显示剩余7条评论

61

有点晚了,但没关系。现在有了C++11,我们有了Lambda和auto变量。我的版本还处理所有空格和空字符串:

#include <cctype>
#include <string>
#include <algorithm>

inline std::string trim(const std::string &s)
{
   auto wsfront=std::find_if_not(s.begin(),s.end(),[](int c){return std::isspace(c);});
   auto wsback=std::find_if_not(s.rbegin(),s.rend(),[](int c){return std::isspace(c);}).base();
   return (wsback<=wsfront ? std::string() : std::string(wsfront,wsback));
}

我们可以使用wsfront构造一个反向迭代器,并将其用作第二个find_if_not的终止条件,但这仅适用于完全由空格组成的字符串,而且至少在gcc 4.8中不够智能,无法使用auto推断出反向迭代器的类型(std::string::const_reverse_iterator)。我不知道构造反向迭代器的成本如何,所以结果可能因人而异。使用这种修改后,代码如下:

inline std::string trim(const std::string &s)
{
   auto  wsfront=std::find_if_not(s.begin(),s.end(),[](int c){return std::isspace(c);});
   return std::string(wsfront,std::find_if_not(s.rbegin(),std::string::const_reverse_iterator(wsfront),[](int c){return std::isspace(c);}).base());
}

3
我总是希望有一个函数调用可以裁剪字符串,而不是自己去实现它。 - linquize
27
就这个问题而言,没有必要使用那个lambda函数。你可以直接传递std::isspace函数:auto wsfront=std::find_if_not(s.begin(),s.end(),std::isspace); - vmrob
5
编译器并不一定很聪明。执行你说的内容是含糊不清的:“候选模板被忽略:无法推导出模板参数'_Predicate' find_if_not(_InputIterator __first, _InputIterator __last, _Predicate __pred)”。 - johnbakers
1
@vmrob 不行,你不能这样做。isspace有两个重载版本。此外,自C++20以来,在标准库中获取函数地址是未定义的行为。 - L. F.
1
@vmrob 另一个重载函数是带有区域设置的。在 C++20 之前,::isspace 函数可以使用(如果包含了 C 标准库头文件)。实际上,另一个问题是在将参数传递给 isspace 之前应该将其强制转换为 unsigned char,但这是另一个故事了。 - L. F.
显示剩余5条评论

30

http://ideone.com/nFVtEo

std::string trim(const std::string &s)
{
    std::string::const_iterator it = s.begin();
    while (it != s.end() && isspace(*it))
        it++;

    std::string::const_reverse_iterator rit = s.rbegin();
    while (rit.base() != it && isspace(*rit))
        rit++;

    return std::string(it, rit.base());
}

这是一个类似复制的解决方案 - 它找到第一个非空格字符的位置(it),然后反转:在只有空格的字符之后的位置(rit) - 然后它返回一个新创建的字符串 == 原始字符串的一部分的副本 - 该部分基于这些迭代器... - jave.web

27

我喜欢tzaman的解决方案,唯一的问题是它不能删除只包含空格的字符串。

为了解决这个问题,在两行修剪代码之间添加str.clear()即可。

std::stringstream trimmer;
trimmer << str;
str.clear();
trimmer >> str;

不错 :) 不过,我们两个解决方案的问题是它们都会修剪两端;不能像这样制作 ltrimrtrim - tzaman
47
不错,但无法处理内部包含空格的字符串。例如,trim(abc def") -> 只剩下 abc,而不是整个字符串。 - coder4
如果您知道不会有任何内部空格,这是一个很好的解决方案! - Elliot Gorokhovsky
1
这很简单易懂,但是由于字符串被复制到和从 std::stringstream 中复制出来,所以速度相当慢。 - Galik
一个经典的修剪(trim)不应该删除内部空格。 - user8143588

27

使用C++17,您可以使用basic_string_view::remove_prefixbasic_string_view::remove_suffix

std::string_view trim(std::string_view s)
{
    s.remove_prefix(std::min(s.find_first_not_of(" \t\r\v\n"), s.size()));
    s.remove_suffix(std::min(s.size() - s.find_last_not_of(" \t\r\v\n") - 1, s.size()));

    return s;
}

一个不错的选择:

std::string_view ltrim(std::string_view s)
{
    s.remove_prefix(std::distance(s.cbegin(), std::find_if(s.cbegin(), s.cend(),
         [](int c) {return !std::isspace(c);})));

    return s;
}

std::string_view rtrim(std::string_view s)
{
    s.remove_suffix(std::distance(s.crbegin(), std::find_if(s.crbegin(), s.crend(),
        [](int c) {return !std::isspace(c);})));

    return s;
}

std::string_view trim(std::string_view s)
{
    return ltrim(rtrim(s));
}

我不确定你在测试什么,但在你的例子中,std::find_first_not_of将返回std::string::npos,而std::string_view::size将返回4。显然最小值是四,这是由std::string_view::remove_prefix要删除的元素数量。gcc 9.2和clang 9.0都正确处理了这个问题:https://godbolt.org/z/DcZbFH - Phidelux

20
在空字符串的情况下,您的代码假设将 1 添加到 string::npos 的结果为 0。string::npos string::size_type 类型,这是无符号类型。 因此,您依赖于加法溢出行为。

25
你的措辞好像这是件坏事一样。有符号整数溢出行为确实是不好的。 - MSalters
4
根据C++标准,将std::string::npos加1必须得到0。因此,可以绝对依赖这个假设。 - Galik

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