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; }
相关文章推荐
- STL 源码剖析 # class string #
- STL学习笔记之迭代器--iterator(源码剖析)
- STL string 的各种功能剖析 以及char字符串与string串的互相转换
- STL 之 map 源码剖析
- STL sort源码剖析
- STL 源码分析之string(一)基础篇
- STL-Map 源码剖析
- STL 源码剖析 算法 stl_algo.h -- search
- Java源码剖析—1 String源码分析
- STL之deque源码剖析
- 【STL】vector源码剖析
- SGI STL中string的源码解读(4)
- STL 之 list 源码剖析
- STL 之 multimap 源码剖析
- 20130331 stl源码剖析之stl分配内存设计
- STL 源码剖析 算法 stl_algo.h -- rotate
- STL 源码剖析 算法 stl_algo.h -- random_shuffle
- 深入源码剖析String,StringBuilder,StringBuffer
- STL源码剖析学习五:list
- Java源码剖析—2 String源码分析