STL string源码剖析

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; }
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
解题思路: 1. 首先定义一个书类(Book),包含书名、单价、出版社等数据成员,这些数据成员都应该设置为私有的。 2. 定义构造函数和析构函数,构造函数可以重载或默认值,允许创建对象时可传参也可不传参,析构函数用于释放对象占用的内存。 3. 定义其他成员函数,比如修改对应数据,输出对应数据等,尽可能丰富类的功能。 4. 定义一个求和函数,计算3本书之和。 5. 在主函数中创建对象,测试类的成员函数、求和函数功能。 粘贴运行结果: 书名:《C++ Primer》 单价:99.9 出版社:人民邮电出版社 书名:《Effective C++》 单价:88.8 出版社:机械工业出版社 书名:《STL源码剖析》 单价:66.6 出版社:电子工业出版社 3本书的总价为:255.3 代码(带适当注释): #include <iostream> #include <cstring> using namespace std; class Book { private: string name; // 书名 double price; // 单价 string publisher; // 出版社 public: // 构造函数 Book(string n = "", double p = 0.0, string pub = "") { name = n; price = p; publisher = pub; } // 析构函数 ~Book() {} // 修改书名 void setName(string n) { name = n; } // 修改单价 void setPrice(double p) { price = p; } // 修改出版社 void setPublisher(string pub) { publisher = pub; } // 输出书名 string getName() { return name; } // 输出单价 double getPrice() { return price; } // 输出出版社 string getPublisher() { return publisher; } // 求和函数,计算3本书之和 static double sum(Book b1, Book b2, Book b3) { return b1.getPrice() + b2.getPrice() + b3.getPrice(); } }; int main() { // 创建3本书的对象 Book book1("C++ Primer", 99.9, "人民邮电出版社"); Book book2("Effective C++", 88.8, "机械工业出版社"); Book book3("STL源码剖析", 66.6, "电子工业出版社"); // 输出3本书的信息 cout << "书名:" << book1.getName() << endl; cout << "单价:" << book1.getPrice() << endl; cout << "出版社:" << book1.getPublisher() << endl << endl; cout << "书名:" << book2.getName() << endl; cout << "单价:" << book2.getPrice() << endl; cout << "出版社:" << book2.getPublisher() << endl << endl; cout << "书名:" << book3.getName() << endl; cout << "单价:" << book3.getPrice() << endl; cout << "出版社:" << book3.getPublisher() << endl << endl; // 计算3本书的总价 double total = Book::sum(book1, book2, book3); cout << "3本书的总价为:" << total << endl; return 0; } 分析总结: 本题主要考察了面向对象编程的基本概念和语法,包括类的定义、构造函数、析构函数、成员函数、静态函数等。同时,还考察了字符串类型的使用和静态成员函数的定义和使用。在实现过程中,需要注意数据成员的访问权限,以及如何在类的外部访问私有成员。此外,还需要注意对象的创建和销毁,以及如何使用静态函数计算3本书的总价。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值