【C++11】字符串拼接之回归原点

栏目: C++ · 发布时间: 5年前

内容简介:在其他语言里,字符串拼接可能是一个常见而且基本不会去注意的部分,但是在C++中字符串拼接有非常多的解决方法。造成这种现象的原因是,C++程序员想要高效地拼接字符串。比如说下面的代码对于有非C/C++语言的人来说可能最平常不过的代码,C++程序员可能直觉上不会采用这种写法。那么C++里面该用什么写法呢?或者说最佳实践是什么?

在其他语言里,字符串拼接可能是一个常见而且基本不会去注意的部分,但是在C++中字符串拼接有非常多的解决方法。造成这种现象的原因是,C++程序员想要高效地拼接字符串。

比如说下面的代码

std::string concat_string(const std::string& name, const std::string& domain) {
    return name + '@' + domain;
}

对于有非C/C++语言的人来说可能最平常不过的代码,C++程序员可能直觉上不会采用这种写法。那么C++里面该用什么写法呢?或者说最佳实践是什么?

这里不会列举各种字符串拼接的方式,如果你有兴趣可以在StackOverflow上搜搜看。个人想要说的是:在分析了C++11里字符串的操作之后个人给出的结论: C++11里最佳的字符串拼接其实就是上述写法 。以下是具体分析。

首先给出一个模拟std::string的MyString类。

class MyString {
public:
    MyString(const char *chars) {
        std::cout << "MyString(char[])\n";
        length_ = std::strlen(chars);
        char *chars_copy = new char[length_ + 1];
        std::copy(chars, chars + length_ + 1, chars_copy);
        chars_ = chars_copy;
    }
 
    explicit MyString(const char *chars, size_t length) {
        std::cout << "MyString(char[], int)\n";
        length_ = length;
        char *chars_copy = new char[length_ + 1];
        std::copy(chars, chars + length_ + 1, chars_copy);
        chars_ = chars_copy;
    }
 
    // copy
    MyString(const MyString &string) {
        std::cout << "MyString(copy)\n";
        length_ = string.length_;
        char *chars_copy = new char[string.length_ + 1];
        std::copy(string.chars_, string.chars_ + length_ + 1, chars_copy);
        chars_ = chars_copy;
    }
 
    // copy assignment
    MyString &operator=(const MyString &) = delete;
 
    // move
    MyString(MyString &&string) noexcept {
        std::cout << "MyString(move), " << string.chars_ << std::endl;
        length_ = string.length_;
        chars_ = string.chars_;
        string.length_ = 0;
        string.chars_ = nullptr;
    }
 
    // move assignment
    MyString &operator=(const MyString &&) = delete;
 
    int length() const { return length_; }
 
    MyString &operator+=(const char *chars) {
        return append(chars);
    }
 
    MyString &operator+=(const MyString &string) {
        return append(string.chars_);
    }
 
    MyString &operator+=(const char ch) {
        std::cout << "MyString#operator+=(char)\n";
        char *new_chars = new char[length_ + 2];
        std::copy(chars_, chars_ + length_, new_chars);
        new_chars[length_] = ch;
        new_chars[length_ + 1] = '\0';
        length_ += 1;
        delete[] chars_;
        chars_ = new_chars;
        return *this;
    }
 
    MyString &append(const char *chars) {
        std::cout << "MyString#append(const char[])\n";
        size_t length = std::strlen(chars);
        char *new_chars = new char[length_ + length + 1];
        std::copy(chars_, chars_ + length_, new_chars);
        std::copy(chars, chars + length + 1, new_chars + length_);
        length_ += length;
        delete[] chars_;
        chars_ = new_chars;
        return *this;
    }
 
    friend std::ostream &operator<<(std::ostream &os, const MyString &string) {
        if (string.chars_ != nullptr) {
            os << string.chars_;
        }
        return os;
    }
 
    ~MyString() {
        std::cout << "~MyString(";
        if (chars_ != nullptr) {
            std::cout << chars_;
        }
        std::cout << ")\n";
        delete[] chars_;
    }
 
private:
    char *chars_;
    size_t length_;
};

MyString类支持copy/move,以及重载了+=操作符。

在MyString的方法实现里面,加了部分debug代码,可以让你理解字符串拼接时实际哪些方法被调用了。

对其他语言背景的人来说,需要知道std::string其实比实际内容多留了一些空间方便追加字符,所以std::string是可变而且空间利用率不是100%。

这里MyString并没有像std::string一样留一些空间,不过这不影响分析。

调用代码如下

int main() {
    MyString email = concat_string(MyString{"foo"}, MyString{"bar.com"});
 
    MyString name{"foo"};
    MyString email2 = concat_string(name, MyString{"bar.com"});
    return 0;
}

如果你想在尝试编译的话,肯定是无法通过的,因为操作符+没有被重载。以下是最基本的操作符重载。

方法前带有friend,所以请写在MyString类里面。

friend MyString operator+(const MyString &lhs, const MyString &rhs) {
    std::cout << "MyString+(const MyString&, const MyString&)\n";
    MyString result{lhs};
    result += rhs.chars_;
    return result;
}
 
friend MyString operator+(const MyString &lhs, const char ch) {
    std::cout << "MyString+(const MyString&, const char)\n";
    MyString result{lhs};
    result += ch;
    return result;
}
 
friend MyString operator+(const MyString &lhs, const char *chars) {
    std::cout << "MyString+(const MyString&, const char*)\n";
    MyString result{lhs};
    result += chars;
    return result;
}

可以看到上述代码,都有一个result的变量。因为输入参数都是const,无法修改。

这里假如你不给参数加const,用临时变量调用的代码无法编译通过,如果干脆const和引用&都不加的话,第二个用栈上变量调用的代码会复制name的内容,这可能不是你想要的。所以方法签名是const T&,代码中也必须执行一次到result复制。

这时编译并执行后得到的第一个调用的输出

MyString(char[])
MyString(char[])
MyString+(const MyString&, const char)
MyString(copy)
MyString#append(const char[])
MyString+(const MyString&, const MyString&)
MyString(copy)
MyString#append(const char[])
~MyString(foo@)
~MyString(bar.com)
~MyString(foo)
~MyString(foo@bar.com)

可以看到copy了两次。理论上这是正确的,因为你+了两次,产生了两个临时result变量。

个人认为,因为上述原因,很多C++程序员可能不会选择开篇的那种写法。从效率上来说,最理想的状态是,只开辟一个result变量,所有字符串都往result拼接。所以产生了如下的几种写法

void concat_string6(const MyString &name, const MyString &domain, MyString &result) {
    result += name;
    result += '@';
    result += domain;
}
 
MyString concat_string7(const MyString &name, const MyString &domain) {
    MyString result{""};
    result += name;
    result += '@';
    result += domain;
    return result;
}

老实说这两种写法没有太大区别。虽然效率可能比较高,但是写法不直观。那么有没有其他的方法呢?

如果你仔细观察C++11引入的新的std::string对应 操作符+重载的方法签名 的话,你可能会发现几个带有T&&的方法签名

template< class CharT, class Traits, class Alloc >
 
    basic_string<CharT,Traits,Alloc>
        operator+( basic_string<CharT,Traits,Alloc>&& lhs,
                   basic_string<CharT,Traits,Alloc>&& rhs );
	(6) 	(since C++11)
template< class CharT, class Traits, class Alloc >
 
    basic_string<CharT,Traits,Alloc>
        operator+( basic_string<CharT,Traits,Alloc>&& lhs,
                   const basic_string<CharT,Traits,Alloc>& rhs );
	(7) 	(since C++11)
template< class CharT, class Traits, class Alloc >
 
    basic_string<CharT,Traits,Alloc>
        operator+( basic_string<CharT,Traits,Alloc>&& lhs,
                   const CharT* rhs );
	(8) 	(since C++11)
template< class CharT, class Traits, class Alloc >
 
    basic_string<CharT,Traits,Alloc>
        operator+( basic_string<CharT,Traits,Alloc>&& lhs,
                   CharT rhs );
	(9) 	(since C++11)
template< class CharT, class Traits, class Alloc >
 
    basic_string<CharT,Traits,Alloc>
        operator+( const basic_string<CharT,Traits,Alloc>& lhs,
                   basic_string<CharT,Traits,Alloc>&& rhs );
	(10) 	(since C++11)
template< class CharT, class Traits, class Alloc >
 
    basic_string<CharT,Traits,Alloc>
        operator+(const CharT* lhs,
                  basic_string<CharT,Traits,Alloc>&& rhs );
	(11) 	(since C++11)
template< class CharT, class Traits, class Alloc >
 
    basic_string<CharT,Traits,Alloc>
        operator+( CharT lhs,
                   basic_string<CharT,Traits,Alloc>&& rhs );
	(12) 	(since C++11)

这些方法签名会带来哪些变化呢?第一个就应该是lhs可以不是const了,也就是说lhs可以修改了!以下是模拟代码(注意返回时必须使用std::move,否则会变成复制。如果有怀疑的话可以看std::string中的源码)

friend MyString operator+(MyString &&lhs, const char *chars) {
    std::cout << "MyString+(MyString&&, const char*)\n";
    lhs += chars;
    return std::move(lhs);
}
friend MyString operator+(MyString &&lhs, const MyString &rhs) {
    std::cout << "MyString+(const MyString&, const MyString&)\n";
    lhs += rhs.chars_;
    return std::move(lhs);
}
 
friend MyString operator+(MyString &&lhs, const char ch) {
    std::cout << "MyString+(MyString&&, char)\n";
    lhs += ch;
    return std::move(lhs);
}

可以看到result变量消失了,所有方法都在修改lhs。那么这是否就意味着传入的参数会直接被修改呢?比如调用代码中的第二种方式。

答案是不会,因为在concat_string方法里name是const String&,所以匹配的是旧方法,而不是新加的这几个方法。

实际执行调用代码(第一种和第二种结果一样)

MyString(char[])
MyString(char[])
MyString+(const MyString&, const char)
MyString(copy)
MyString#operator+=(char)
MyString+(MyString&&, const MyString&)
MyString#append(const char[])
MyString(move)
~MyString()
~MyString(bar.com)
~MyString(foo@bar.com)
~MyString(foo)

可以很清晰地看到,新方法在第二个+的时候被调用了。又由于新方法中不会复制,所以是一次copy加一次move。

对比一下,不通过concat_string而是直接调用的方式

// MyString email = MyString{"foo"} + '@' + MyString{"bar.com"};
 
MyString(char[])
MyString+(MyString&&, char)
MyString#operator+=(char)
MyString(move)
MyString(char[])
MyString+(MyString&&, const MyString&)
MyString#append(const char[])
MyString(move)
~MyString(bar.com)
~MyString()
~MyString()
~MyString(foo@bar.com)

临时变量的话,两次move。

// MyString foo = MyString{"foo"};
// MyString email = foo + '@' + MyString{"bar.com"};
 
MyString(char[])
MyString+(const MyString&, const char)
MyString(copy)
MyString#operator+=(char)
MyString(char[])
MyString+(MyString&&, const MyString&)
MyString#append(const char[])
MyString(move)
~MyString(bar.com)
~MyString()
~MyString(foo@bar.com)
~MyString(foo)

一次copy,一次move。

小结一下,在C++11中,字符串的拼接支持了move,减少了copy的次数。

如果你要问是否可以不copy?那么请考虑下,C++11之前没有copy的那两种写法,即“ 创建一个result中间变量加上第一次字符串拼接” 与“ 用第一个字符串复制构造一个中间变量 ”其实本质上没有区别,重要的是中间没有重复创建result变量就行。

作为参考,其他几种可能的写法

// copy, move, best
MyString concat_string(const MyString &name, const MyString &domain) {
    return name + "@" + domain;
}
// move, move, move
MyString concat_string2(const MyString &name, const MyString &domain) {
    return MyString{""} + name + "@" + domain;
}
// move, move
MyString concat_string3(MyString &&name, const MyString &domain) {
    return std::move(name) + "@" + domain;
}
// move
MyString concat_string4(MyString &&name, const MyString &domain) {
    name += "@";
    name += domain;
    return std::move(name);
}
// copy
MyString concat_string5(MyString &&name, const MyString &domain) {
    name += "@";
    name += domain;
    return name;
}
// no copy, no move
void concat_string6(const MyString &name, const MyString &domain, MyString &result) {
    result += name;
    result += "@";
    result += domain;
}
// no copy, no move
MyString concat_string7(const MyString &name, const MyString &domain) {
    MyString result{""};
    result += name;
    result += "@";
    result += domain;
    return result;
}

效率上没有太大区别,但是相信你也会认为第一种是最好的。

最后,C++11增加的字符串拼接中,包含了以下两个方法

template< class CharT, class Traits, class Alloc >
 
    basic_string<CharT,Traits,Alloc>
        operator+(const CharT* lhs,
                  basic_string<CharT,Traits,Alloc>&& rhs );
	(11) 	(since C++11)
template< class CharT, class Traits, class Alloc >
 
    basic_string<CharT,Traits,Alloc>
        operator+( CharT lhs,
                   basic_string<CharT,Traits,Alloc>&& rhs );
	(12) 	(since C++11)

也就是说,你可以这么写代码

"foo" + std::to_string(123);
'f' + std::to_string(123);

而不用显示的去用const char*去构造一个std::string。

总结

字符串拼接是开发中常见的代码,所以这种基础代码中在C++11中能按照直觉写而不用担心效率着实是一件很好的事情,有一种写了那么长时间便扭的C++代码回到了原点的感觉。最后希望我的分析对各位有用。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Parsing Techniques

Parsing Techniques

Dick Grune、Ceriel J.H. Jacobs / Springer / 2010-2-12 / USD 109.00

This second edition of Grune and Jacobs' brilliant work presents new developments and discoveries that have been made in the field. Parsing, also referred to as syntax analysis, has been and continues......一起来看看 《Parsing Techniques》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试