从实际使用上来说,std::string跟应该叫做std::string_buffer,因为它的特性更像std::vector而不是std::array,在很多情况下都容易引起数据拷贝。在实际项目中,拷贝std::vector
- boost::string_ref(chrome:StringPiece)
- std::string_view(c++17后才可用)
- 不直接传递一个string,而是传递它的迭代器
本文主要来分析GNU STL中std::string的实现,string_ref的实现可参考string_ref的实现,folly::FBString的实现可参考FBString的实现
定义
以gcc-stl 4.4.0
为例std::string的实现方式,gnu-stl中string的定义如下:1
2
3
4
5
template<typename _CharT, typename _Traits = char_traits<_CharT>,
typename _Alloc = allocator<_CharT> >
class basic_string;
typedef basic_string<char> string;
可以看到,std::string是basic_string关于char的实现,下面来看basic_string的实现,如下为关于basic_string的部分说明:
1 | * A string looks like this: |
构造
首先来看basic_string的构造函数,以最常见的std::string(“xxx”)这种构造方式为例来看它对应的构造函数:
1 | template<typename _CharT, typename _Traits, typename _Alloc> |
它调用_S_construct
这个函数对_M_dataplus
这个成员变量来进行初始化,_M_dataplus
的定义为:1
2
3
4
5
6
7
8
9
10
11
12
13
// Use empty-base optimization: http://www.cantrip.org/emptyopt.html
struct _Alloc_hider : _Alloc
{
_Alloc_hider(_CharT* __dat, const _Alloc& __a)
: _Alloc(__a), _M_p(__dat) { }
_CharT* _M_p; // The actual data.
};
private:
// Data Members (private):
mutable _Alloc_hider _M_dataplus;
由此可见,_M_dataplus
中包含指向实际数据的指针,前文也说到,basic_string中应该还包含有大小、容量等数据成员的,这部分数据成员是存在_Rep
中的,basic_string只存储指向它的指针,从而将减少内存占用和避免额外的内存拷贝,它的定义如下所示:
1 |
|
_S_construct
根据输入参数初始化_M_dataplus
,定义如下:
1 | template<typename _CharT, typename _Traits, typename _Alloc> |
其中,_Rep::_S_create
的定义如下:
1 | template<typename _CharT, typename _Traits, typename _Alloc> |
到现在可以明白的是,_Rep
中存储了前文说到的字符串长度、容量、引用计数等信息,同时会申请__capacity + 1
大小的内存用以存储实际字符串内容。至此,整个basic_string的初始化机制就比较明朗了。
内存布局
一个std::string
的内存布局如下:
拷贝
在gcc4.*版本中,basic_string的拷贝采用了COW
机制,通过如下代码可以来验证:
1 |
|
在GCC 4.8.5
上编译后的运行结果如下所示:
1 | 1 |
由此可验证字符串拷贝默认是采用了COW
机制,下面来看字符串拷贝的具体实现,它的拷贝构造函数为:
1 | basic_string(const basic_string& __str) |
可以看到,最终调用的_M_refcopy
这个函数,将引用计数加1,然后将指针指向_Rep
,下面来看实际的修改的行为。
1 | reference operator []( size_type __pos ) |
所以数据拷贝的发生在数据实际修改的时候。
总结
- basic_string仅仅包含一个成员
_M_dataplus
,它会指向一个_Rep
的结构,_Rep
中才会实际存放字符串的内容和长度等信息。初始化过程中,对于短字符串,会先存放在栈中,待生成_Rep
指针后才会将数据拷贝至_Rep
中,这样可以避免初始化短字符串的时候去申请动态内存空间 _Rep
中记录了basic_string的引用计数,在GCC4.*
版本中,引用计数是默认开启的,所以对象的拷贝构造很快- 因为basic_string中仅包含指向
_Rep
的指针这一个数据成员,所以sizeof(std::string)
等于8,但是由于它还要为它分配_Rep
,一个空_Rep
的大小为25,所以在GCC4.*
中一个空string的大小也为33个字节 COW
能有效减少内存拷贝,但是其实现较为复杂,也存在一些其他问题,具体可参考[std::string的Copy-on-Write:不如想象中美好),加上新的标准库中std::move
的引入导致COW
的这种优化不再有不要,所以从GCC5开始已经废弃了COW
机制