您的位置:首页 > 其它

STL string源码剖析

2017-07-28 16:06 387 查看

vector与string

这个是源码对为什么string类不定义成vector的解释

// There are three reasons why basic_string is not identical to

// vector. First, basic_string always stores a null character at the

// end; this makes it possible for c_str to be a fast operation.

// Second, the C++ standard requires basic_string to copy elements

// using char_traits<>::assign, char_traits<>::copy, and

// char_traits<>::move. This means that all of vector<>’s low-level

// operations must be rewritten. Third, basic_string<> has a lot of

// extra functions in its interface that are convenient but, strictly

// speaking, redundant.

大概上面意思是总共有3个理由

第一string总是要存储一个\0,用来快速调用c_str函数。

第二c++标准需要string类去用char_traits类去拷贝元素和搬移元素.这样就意味着vector的搬移和拷贝操作必须重新设计。

第三string类有大量额外的函数接口可供便利的调用,但是严格的来说……这些接口是多余的。

Basic_string

// Class basic_string.

// Class invariants:

// (1) [start, finish) is a valid range. 在这个范围是可使用变量的范围

// (2) Each iterator in [start, finish) points to a valid object

// of type value_type. 迭代器在这个范围是合法的

// (3) *finish is a valid object of type value_type; in particular,

// it is value_type(). *finish 也是合法的,但返回的只是一个临时变量

// (4) [finish + 1, end_of_storage) is a valid range. 这个范围是合法的

// (5) Each iterator in [finish + 1, end_of_storage) points to

// unininitialized memory. 任何迭代器访问 该区域内存都是合法的但是都是未初始化的内存

// Note one important consequence: a string of length n must manage

// a block of memory whose size is at least n + 1.

需记住一个重要结论,那就是一个长度为n的string 它的大小长度 n+1

npos 它是一个const 类型的静态变量在string中 大小为无符号整形的最大值

通常在拷贝构造中代表直到拷贝到该字符串结尾。

Basic_string类的空间配置器

其实Basic_string有俩个空间配置器的类,但是string默认使用的是如下这个版本,这个string的空间配置器很像SGI_STL中的simple_alloc这个类,这是对STL中的空间配置器的一个转调接口.
template <class _Tp, class _Alloc> class _String_base {
public:
typedef _Alloc allocator_type;
allocator_type get_allocator() const { return allocator_type(); }

protected:
typedef simple_alloc<_Tp, _Alloc> _Alloc_type;

_Tp* _M_start;
_Tp* _M_finish;
_Tp* _M_end_of_storage;
// Precondition: 0 < __n <= max_size().

_Tp* _M_allocate(size_t __n) { return _Alloc_type::allocate(__n); }
void _M_deallocate(_Tp* __p, size_t __n) {
if (__p)
_Alloc_type::deallocate(__p, __n);
}

void _M_allocate_block(size_t __n) {
if (__n <= max_size()) {
_M_start  = _M_allocate(__n);
_M_finish = _M_start;
_M_end_of_storage = _M_start + __n;
}
else
_M_throw_length_error();
}

void _M_deallocate_block()
{ _M_deallocate(_M_start, _M_end_of_storage - _M_start); }

size_t max_size() const { return (size_t(-1) / sizeof(_Tp)) - 1; }

_String_base(const allocator_type&)
: _M_start(0), _M_finish(0), _M_end_of_storage(0) { }

_String_base(const allocator_type&, size_t __n)
: _M_start(0), _M_finish(0), _M_end_of_storage(0)
{ _M_allocate_block(__n); }

~_String_base() { _M_deallocate_block(); }

void _M_throw_length_error() const;
void _M_throw_out_of_range() const;
};


以下由c++手册上的string接口对应的源码分析

string与Basic_string的关系

template <class _CharT,
class _Traits = char_traits<_CharT>,
class _Alloc = __STL_DEFAULT_ALLOCATOR(_CharT) >
class basic_string;
//这个_Traits参数极为重要string类的copy和assign搬移也是通过它来实现的
typedef basic_string<char>    string;
typedef basic_string<wchar_t> wstring;


1. 要注意上面的声明 string 只是Basic_string的一个实例化类,所以下面所有Basic_string 中有关空配的参数都可以忽视.

2. 这里提醒下,在string中它的迭代器类型就是char * 这个内置类型的指针,所以大家自己下面看这部分源码时在碰见转调其他函数时,直接看对应 const char* 这个参数的函数即可.

Basic_string中所使用的类型声明

typedef _CharT value_type;  // _CharT 代表 char
typedef _Traits traits_type;// _Traits 就是char_traits<char>
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;   // ptrdiff_t在cstddef头文件中它是c++标准指定的类型表内置类型指针的距离
typedef const value_type*           const_iterator;
typedef value_type*                 iterator;


string 之构造初始化

string()

///////////////////////////////////下面是源码所表示的内容/////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
explicit basic_string(const allocator_type& __a = allocator_type()) // 当不指定大小时,编译器默认
: _Base(__a, 8) { _M_terminate_string(); }//这里默认申请了8个元素的空间,即8个char所以是8字节大小的空间。忽视这个空间配置器参数后就可以看出它是string的默认构造函数。
/*terminate_string 表示 string字符串结束符的一个构造函数*/
void _M_terminate_string() {
__STL_TRY {
_M_construct_null(_M_finish);//初始化时 finish和start指向同一位置,大家可以从我上面放的空配代码看出
}
__STL_UNWIND(destroy(_M_start, _M_finish));
}

//下面这个函数只是一个初始化 string 结束符的函数,里面只是在已分配的空间上调用了char()来初始化空串
void _M_construct_null(_CharT* __p) {
construct(__p);  /* 里面在p所指向的空间上调用了placement new ,就仅仅只是调用了 charT() 的默认构
造函数来初始化了p所指的空间,所以用char的默认构造函数来初始化空串!!! 所以初始化后第一个元素为\0 */
#   ifdef __STL_DEFAULT_CONSTRUCTOR_BUG
__STL_TRY {
*__p = (_CharT) 0;
}
__STL_UNWIND(destroy(__p));
#   endif
}
// 所以可以看出string的结束符是有的,为char()的值也是0。


其实如上可以看出,string的默认构造函数,只是申请了8字节大小的空间,并且第一个元素初始化为0,剩下8个元素都是未初始化的状态。


string的拷贝构造

以下的所有string的拷贝构造都用到了这个函数,我给大家在这里先分析以下这个函数的意义
template <class _ForwardIter>
void _M_range_initialize(_ForwardIter __f, _ForwardIter __l,
forward_iterator_tag) {
difference_type __n = 0;
distance(__f, __l, __n);
_M_allocate_block(__n + 1);
_M_finish = uninitialized_copy(__f, __l, _M_start);
_M_terminate_string(); //最后在把finish所指向元素初始化为 \0
}
首先这个distance函数是用来这求俩个迭代器之间的元素个数,然后调用了string的空间配置器申请了从first到last迭代器所指向这么多元素的空间。然后调用了uninitialized_copy。
ForwardIter uninitialized_copy(InputIter first,InputIter last,ForwardIter result);
这个函数其实就是用参数first,last所指区间的所有元素来初始化result所指向的空间。然后把result返回出去.
而且在这个函数中是会先萃取了下,如果是POD类型直接调用了memcpy来初始化,如果不是一个一个调用拷贝构造.


string (const string& str, size_t pos, size_t len = npos)

basic_string(const basic_string& __s, size_type __pos, size_type __n = npos,
const allocator_type& __a = allocator_type())
: _Base(__a) {
if (__pos > __s.size())
_M_throw_out_of_range();
else
_M_range_initialize(__s.begin() + __pos,
__s.begin() + __pos + min(__n, __s.size() - __pos));
} // s.size()-pos代表如果 pos 位后直到结尾的长度。所以综上如果这个长度比参数len小选择这个长度,否则选择参数len。所以也就是我们用拷贝构造的方式裁剪字符串时,len如果传的大于剩余字符的长度,string最多从pos位拷贝到字符串的结尾.


string (const char* s)

basic_string(const basic_string& __s) : _Base(__s.get_allocator())
{ _M_range_initialize(__s.begin(), __s.end()); }
//这个简单的从头拷贝到尾


string (const char* s, size_t n)

basic_string(const _CharT* __s, size_type __n,
const allocator_type& __a = allocator_type())
: _Base(__a)
{ _M_range_initialize(__s, __s + __n); } //其实也就申请了n+1个空间,拷贝了n个有效字符进去。


string (size_t n, char c)

basic_string(size_type __n, _CharT __c,
const allocator_type& __a = allocator_type())
: _Base(__a, __n + 1)
{
_M_finish = uninitialized_fill_n(_M_start, __n, __c);
_M_terminate_string();
}


string类析构

~basic_string() { destroy(_M_start, _M_finish + 1); }
这里只string的析构函数只对空间进行了析构并未释放空间,当string类的基类的析构函数被调用时,由其基类来释放空间


string类的赋值运算符重载

_Traits ------ class _Traits = char_traits<_CharT>
static _CharT* copy(_CharT* __s1, const _CharT* __s2, size_t __n) {
memcpy(__s1, __s2, __n * sizeof(_CharT));
return __s1;
}
template <class _CharT, class _Traits, class _Alloc>
basic_string<_CharT,_Traits,_Alloc>&
basic_string<_CharT,_Traits,_Alloc>::assign(const _CharT* __f,
const _CharT* __l)
{
const ptrdiff_t __n = __l - __f; //这里的n为有效字符大小不包括\0
if (static_cast<size_type>(__n) <= size()) {
_Traits::copy(_M_start, __f, __n);//列如 A=B, B字符串比A字符串短,就把B的有效字符从A的头
erase(_M_start + __n, _M_finish);//部开始拷贝,一直拷贝到结束。然后把原字符串之后的有效元素全部析构掉
}//之后新的被赋值的字符串大小为n的大小
else {
_Traits::copy(_M_start, __f, size());// 否则先拷贝大小为size个字符在A字符串中
append(__f + size(), __l);//再调用扩展函数拷贝剩下的...这个里面扩容了
}
return *this;
}
/*综上 assign函数,当比原字符串有效大小,小时会裁剪原字符串,比原字符串大时,会扩展原字符串*/
1. basic_string& operator=(const basic_string& __s) {
if (&__s != this)
assign(__s.begin(), __s.end());
return *this;
}
2.  basic_string& operator=(const _CharT* __s) //处理与1相同
{ return assign(__s, __s + _Traits::length(__s)); }
3.  basic_string& operator=(_CharT __c)
{ return assign(static_cast<size_type>(1), __c); }
//把原字符串的第一个字符赋值成C。并且只保留字符C的空间,其余的有效元素的空间全释放掉。
// eg s1("AAAA")  s1='b'  s1现在结果只有字符b s1=='b' 为True


综上

无论如何都会赋值成功。

假设俩个字符串赋值:

A = B,B串给A串赋值

如果B长度小于A,那么就把B的所有的有效字符拷贝到A字符串中。从A的头部开始拷贝,直到B字符串结束。

如果B长度大于A,那么A字符串释放之前所使用的空间。重新申请一块更大的空间用来存放B的所有有效字符。

string之容量

size_t size()const

size_type请看前面的类型声明
size_type size() const { return _M_finish - _M_start; } //有效字符的个数,finish永远指向\0


size_t length() const

size_type length() const { return size(); }


size_t max_size() const

size_t max_size() const { return (size_t(-1) / sizeof(_Tp)) - 1; }
其实max_size就是无符号整数的最大值-1。


void resize (size_t n) / void resize (size_t n, char c)

void resize(size_type __n, _CharT __c) {
if (__n <= size())    // 当扩展大小,小于原大小时只裁剪原字符串,原字符串被裁剪为只有 n个字符
erase(begin() + __n, end());
else   //当扩展大小大于原大小时才在原字符串后面扩展,此时原字符串大小变为n,扩展字符个数为n-size()个c。
append(__n - size(), __c);
}

void resize(size_type __n)
{ resize(__n, _M_null()); }
//这个跟上面一样只是如果n大于原字符串大小时,扩展字符为\0而已。
从上可看resize函数会改变原字符串大小。


size_t capacity() const

size_type capacity() const
{ return (_M_end_of_storage - _M_start) - 1; }
如果容量没变,这个大小为你第一次申请空间时所传的大小


void reserve (size_t n = 0)

用来修改string容量的一个函数大家,不必要纠结这个函数,因为不同版本的STL,string的扩容方式可能不同。
void basic_string<_CharT,_Traits,_Alloc>::reserve(size_type __res_arg) {
if (__res_arg > max_size())
_M_throw_length_error();

size_type __n = max(__res_arg, size()) + 1;//设n为重新扩容的大小和原有效字符中的最大值
pointer __new_start = _M_allocate(__n);//然后释放之前空间。再重新申请大小为n的空间,并把之前的元素拷贝过来
pointer __new_finish = __new_start;

__STL_TRY {
__new_finish = uninitialized_copy(_M_start, _M_finish, __new_start);
_M_construct_null(__new_finish);
}
__STL_UNWIND((destroy(__new_start, __new_finish),
_M_deallocate(__new_start, __n)));

destroy(_M_start, _M_finish + 1);
_M_deallocate_block();
_M_start = __new_start;
_M_finish = __new_finish;
_M_end_of_storage = __new_start + __n;
}
所以该函数肯定会发生迭代器失效....
其次原字符串内容不变,有效字符个数不变,但是容量改变了。
如果重新设置的容量大小比原字符串的有效字符个数小,容量改变为原字符串有效字符大小,否则改变为重设容量大小.
特别强调:vs 和 sgi 的string类的扩容方式不同且reserve实现也不同需要注意下。


void clear()

void clear() {
if (!empty()) {
_Traits::assign(*_M_start, _M_null());//原字符串的第一个字符赋值成\0,然后析构之后的有效字符
destroy(_M_start+1, _M_finish+1);//再让finish指向start,此时size==0
_M_finish = _M_start;
}
}


bool empty() const

bool empty() const { return _M_start == _M_finish; }


所有容量函数结束,下面是string的模型



append — 扩展string对象的函数

string& append (const char* s)

basic_string& append(const _CharT* __s)
{ return append(__s, __s + _Traits::length(__s)); }
这个length求的只是有效字符长度,所以它把第一个字符的位置和\0的位置传了过去,然后转调了
append(const char*first,const * last) 这个函数,关于这个函数在下面有详细的分析。


string& append (const char* s, size_t n)

basic_string& append(const _CharT* __s, size_type __n)
{ return append(__s, __s+__n); }
其实跟上面这个函数基本一样,只不过一个拷贝全部字符串,一个拷贝字符串的子串到string中


string& append (size_t n, char c)

basic_string<_CharT,_Traits,_Alloc>::append(size_type __n, _CharT __c) {
if (__n > max_size() || size() > max_size() - __n)
_M_throw_length_error();
if (size() + __n > capacity())
reserve(size() + max(size(), __n));
if (__n > 0) {
uninitialized_fill_n(_M_finish + 1, __n - 1, __c);// 这里先拷贝n-1个跟下面的先start++再拷贝一个道理
__STL_TRY {//都是防止这里抛出异常又把原先的\0给覆盖掉导致原字符串无效.
_M_construct_null(_M_finish + __n);
}
__STL_UNWIND(destroy(_M_finish + 1, _M_finish + __n));
_Traits::assign(*_M_finish, __c);
_M_finish += __n;
}
return *this;
}


string& append (const string& str)

首先 begin end 都返回的是一个 iter 类型 ,这个类型是 charT* 的一个别名,所以就是char*
basic_string& append(const basic_string& __s)
{ return append(__s.begin(), __s.end()); }
// 故这里就相当转调append(const char*,cosnt char*)的一个函数


string& append (const string& str, size_t subpos, size_t sublen)

该函数表示从pos位置开始拷贝n个元素到dest string 中.
basic_string& append(const basic_string& __s, size_type __pos, size_type __n)
{ //pos指在字符串中的下标 n指从该下标开始拷贝多少个元素
if (__pos > __s.size()) //当pos大于src字符串的有元素个数时抛出异常
_M_throw_out_of_range();
return append(__s.begin() + __pos,__s.begin() + __pos + min(__n, __s.size() - __pos));
}
然后转调 append(const char * first,const char* last)这个函数
这里注意下只要pos合法无论n是否合法都可以调用成功,如果n个数大于从pos到finish这个[)区间的有效元素个数的话,最多就把pos到\0这个区间的所有有效元素拷贝到dest string中。如果小于就拷贝从pos开始拷贝相应大小的字符串过去.


append(const char* first,cosnt char*last) 该函数并没有暴露给外层基本上所有append的函数都是转调它

basic_string<_Tp, _Traits, _Alloc>::append(const _Tp* __first,
const _Tp* __last)
{
if (__first != __last) { //先看是不是空串
const size_type __old_size = size();
ptrdiff_t __n = __last - __first; //这个 n 代表有效元素的个数
if (__n > max_size() || __old_size > max_size() - __n)
_M_throw_length_error();
if (__old_size + __n > capacity()) {//如果原先容量不足进入这个代码块
const size_type __len = __old_size + max(__old_size, (size_t) __n) + 1;
pointer __new_start = _M_allocate(__len);
pointer __new_finish = __new_start;
__STL_TRY {
__new_finish = uninitialized_copy(_M_start, _M_finish, __new_start);
__new_finish = uninitialized_copy(__first, __last, __new_finish);
_M_construct_null(__new_finish);
}// 这里其实就申请了段满足新的大小的空间,然后把老的元素拷贝进去,再把要赋值的元素拷贝进去
__STL_UNWIND((destroy(__new_start,__new_finish),
_M_deallocate(__new_start,__len)));
destroy(_M_start, _M_finish + 1);
_M_deallocate_block();
_M_start = __new_start;
_M_finish = __new_finish;
_M_end_of_storage = __new_start + __len;
} /*上面是当之前的字符串容量不足以容纳新的字符串的处理*/
else {
const _Tp* __f1 = __first;
++__f1;// 这个first ++ 是正确的,之前我思考了好久在这里,终于想通了,这里先++的原因如下,先++的操作如下图
uninitialized_copy(__f1, __last, _M_finish + 1);
__STL_TRY {
_M_construct_null(_M_finish + __n);//这里先拷贝start+1后面元素,在回头用start在覆盖掉原先的\0
}//主要是因为这里的 try 中的构造\0的这个函数,如果它抛出异常,刚拷贝的元素都会析构掉,所以原字符串还是有效的
__STL_UNWIND(destroy(_M_finish + 1, _M_finish + __n));//如果我们直接从finish处赋值的话抛出异常后
_Traits::assign(*_M_finish, *__first);//原字符串的\0就被覆盖掉了,原字符串就不再是有效字符串就会显示热热热
_M_finish += __n;
}
}


解释上面如何要先start++一下再赋值





string 运算符重载函数

string& operator+= (const string& str); —> string

basic_string& operator+=(const basic_string& __s)
{ return append(__s); }


string& operator+= (const char* s); —> c-string

basic_string& operator+=(const _CharT* __s)
{ return append(__s); }


string& operator+= (char c); —>character

basic_string& operator+=(_CharT __c)
{ push_back(__c); return *this; }
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: