STL string源码分析
Posted 小张的code世界
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STL string源码分析相关的知识,希望对你有一定的参考价值。
“ 静水流深,只是无意起波澜。”
我希望大家读完这篇文章能够清楚一下问题:
-
string的常见的实现方式有几种?
-
string类的内部结构是什么样子?
-
string内部使用的内存是如何分配管理的?
-
string是如何拷贝构造,如何析构的,有引用计数的概念吗?
-
string都给我们提供了哪些关键的API?
-
string的data()和c_str()函数有什么区别?
-
std::to_string()是如何实现的?
0. 字符串常量
在开始string的源码分析之前,我们需要明确一些和字符串相关的概念。首先就是字符常量与字符串常量。
字符常量
字符常量指的是用单撇号括起来的一个字符就是字符型常量。如'a', '#', '%', 'D'都是合法的字符常量,在内存中占一个字节。
字符常量只能包括一个字符,如'AB' 是不合法的。
字符常量区分大小写字母,如'A'和'a'是两个不同的字符常量。
撇号(')是定界符,而不属于字符常量的一部分。如cout<<'a';输出的是一个字母"a",而不是3个字符"'a' "。
将一个字符常量存放到内存单元时,实际上并不是把该字符本身放到内存单元中去,而是将该字符相应的ASCII代码放到存储单元中。如果字符变量c1的值为'a',c2的值为'b',则在变量中存放的是'a'的ASCII码97,'b' 的ASCII码98。
既然字符数据是以ASCII码存储的,它的存储形式就与整数的存储形式类似。这样,在C++中字符型数据和整型数据之间就可以通用。一个字符数据可以赋给一个整型变量,反之,一个整型数据也可以赋给一个字符变量。也可以对字符数据进行算术运算,此时相当于对它们的ASCII码进行算术运算。
字符串常量
字符串常量指的是用双撇号括起来字符,字符串常量存储在静态存储区中,如"abc","Hello!","a+b","Li ping"等都是字符串常量。编译系统会在字符串最后自动加一个'\0'作为字符串结束标志。但'\0'并不是字符串的一部分,它只作为字符串的结束标志。例如,字符串常量"abc"在内存中占4个字节(而不是3个字节)。
char* s1 = "abc";
char s2[4] = "abc";
1. STL string源码分析
string的内容主要在gcc源码的三个文件中:<string>、<basic_string.h>、<basic_string.tcc>。我们首先要明白,STL中的string是一个类型,我们用string声明的变量都是一个string类型的对象。因为<string>中对string是这样定义的
typedef basic_string<char> string;
// 其中basic_string的定义如下
template<typename _CharT, typename _Traits = char_traits<_CharT>,
typename _Alloc = allocator<_CharT> >
class basic_string;
看到了吧,我们使用的string其实是模板类basic_string的特化版本basic_string<char>。对string的源码分析实际上就是对basic_string<char>的源代码分析。basic_string源代码的简化版本如下
template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string
{
// 保存字符串对象的内容的地址
_Alloc_hider _M_dataplus;
// 字符串对象内容的长度
size_type _M_string_length;
// _S_local_capacity是默认存储能力,存储数组的最大能存储字符数
// 区别于_M_string_length
enum { _S_local_capacity = 15 / sizeof(_CharT) };
/**
* 这里有个小技巧,用了union
* 因为使用_M_local_buf时候不需要关注_M_allocated_capacity
* 使用_M_allocated_capacity时就不需要关注_M_local_buf
* 继续向下看就会明白。
*/
union
{
_CharT _M_local_buf[_S_local_capacity + 1];
size_type _M_allocated_capacity;
}
// 其中_Alloc_hider的定义如下
struct _Alloc_hider : allocator_type
{
_Alloc_hider(pointer __dat, const _Alloc& __a = _Alloc())
: allocator_type(__a), _M_p(__dat) { }
_Alloc_hider(pointer __dat, const _Alloc& __a)
: allocator_type(__a), _M_p(__dat) { }
// _M_p指向实际的内容
pointer _M_p;
}
// 几个比较重要的函数
// 用来修改_M_p的指向
void _M_data(pointer __p)
{ _M_dataplus._M_p = __p; }
// 用来修改长度_M_string_length
void _M_length(size_type __length)
{ _M_string_length = __length; }
// 用来返回_M_p
pointer _M_data() const
{ return _M_dataplus._M_p; }
// 返回保存字符串内容的数组的地址
pointer _M_local_data()
{ return pointer(_M_local_buf); }
// 返回最大可以存储内容的长度
void _M_capacity(size_type __capacity)
{ _M_allocated_capacity = __capacity; }
};
有了上面的基础后我们就可以来看构造函数了,string有很多构造函数,也就有很多种构造方式。首先,看一下默认构造函数,是将 _M_p指向了_M_local_buf
// 默认构造函数,将开辟的数组的地址赋给_M_p,数组的长度为16
// 长度为0
// string str;这种方式就是调用这个构造函数
basic_string() :_M_dataplus(_M_local_data())
{ _M_set_length(0); }
// _M_local_data()函数的实现如下
const_pointer _M_local_data() const {
return std::pointer_traits<const_pointer>::pointer_to(*_M_local_buf);
}
其他的构造函数如下,底层都是调用_M_construct函数,关于这个函数的解析请看第二节。
// 拷贝构造函数
basic_string(const basic_string& __str)
: _M_dataplus(_M_local_data(),
_Alloc_traits::_S_select_on_copy(__str._M_get_allocator()))
{ _M_construct(__str._M_data(), __str._M_data() + __str.length()); }
// 其他构造函数
basic_string(const basic_string& __str, size_type __pos, const _Alloc& __a = _Alloc())
: _M_dataplus(_M_local_data(), __a)
{
const _CharT* __start = __str._M_data()
+ __str._M_check(__pos, "basic_string::basic_string");
_M_construct(__start, __start + __str._M_limit(__pos, npos));
}
basic_string(const basic_string& __str, size_type __pos, size_type __n)
: _M_dataplus(_M_local_data())
{
const _CharT* __start = __str._M_data()
+ __str._M_check(__pos, "basic_string::basic_string");
_M_construct(__start, __start + __str._M_limit(__pos, __n));
}
// 使用C字符串进行string对象的构造
basic_string(const _CharT* __s, size_type __n, const _Alloc& __a = _Alloc())
: _M_dataplus(_M_local_data(), __a)
{ _M_construct(__s, __s + __n); }
basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())
: _M_dataplus(_M_local_data(), __a)
{ _M_construct(__s, __s ? __s + traits_type::length(__s) : __s+npos); }
// 使用n个字符构造
basic_string(size_type __n, _CharT __c, const _Alloc& __a = _Alloc())
: _M_dataplus(_M_local_data(), __a)
{ _M_construct(__n, __c); }
// 使用列表初始化的方式
basic_string(initializer_list<_CharT> __l, const _Alloc& __a = _Alloc())
: _M_dataplus(_M_local_data(), __a)
{ _M_construct(__l.begin(), __l.end()); }
析构函数,实际就是释放_M_p所指向的内存。
~basic_string()
{ _M_dispose(); }
void _M_dispose()
{
/**
* 如果当前指向的是本地内存那15个字节,则不需要释放
* 如果不是,则需要使用_M_destroy去释放其指向的内存
*/
if (!_M_is_local())
_M_destroy(_M_allocated_capacity);
}
/**
* 判断下当前内部指向的是不是本地内存
* _M_local_data()即返回_M_local_buf的地址
*/
bool _M_is_local() const { return _M_data() == _M_local_data(); }
void _M_destroy(size_type __size) throw()
{ _Alloc_traits::deallocate(_M_get_allocator(), _M_data(), __size + 1); }
赋值构造函数,STL中有这样一句话
// Propagating allocator cannot free existing storage so must
// deallocate it before replacing current allocator.
basic_string& operator=(const basic_string& __str)
{
// 底层都是调用assign
return this->assign(__str);
}
// assign
basic_string&
assign(const basic_string& __str)
{
this->_M_assign(__str);
return *this;
}
// 核心函数_M_assign
template<typename _CharT, typename _Traits, typename _Alloc>
void basic_string<_CharT, _Traits, _Alloc>::
_M_assign(const basic_string& __str)
{
if (this != &__str)
{
const size_type __rsize = __str.length();
const size_type __capacity = capacity();
/**
* 如果capacity不够用,需要进行重新分配
*/
if (__rsize > __capacity)
{
size_type __new_capacity = __rsize;
pointer __tmp = _M_create(__new_capacity, __capacity);
_M_dispose();
_M_data(__tmp);
_M_capacity(__new_capacity);
}
/**
* 将__str指向的内存拷贝到当前对象指向的内存上
*/
if (__rsize)
this->_S_copy(_M_data(), __str._M_data(), __rsize);
_M_set_length(__rsize);
}
}
移动构造函数的源代码分析如下:
/**
* 移动构造函数,其实就是把src指向的内存移动到了dst种
*/
basic_string(basic_string&& __str) noexcept : _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator())) {
if (__str._M_is_local()) {
traits_type::copy(_M_local_buf, __str._M_local_buf, _S_local_capacity + 1);
} else {
_M_data(__str._M_data());
_M_capacity(__str._M_allocated_capacity);
}
// Must use _M_length() here not _M_set_length() because
// basic_stringbuf relies on writing into unallocated capacity so
// we mess up the contents if we put a '\0' in the string.
_M_length(__str.length());
__str._M_data(__str._M_local_data());
__str._M_set_length(0);
}
string为我们提供的优美的API如下
basic_string&
replace(size_type __pos, size_type __n, const basic_string& __str)
{ return this->replace(__pos, __n, __str._M_data(), __str.size()); }
size_type
copy(_CharT* __s, size_type __n, size_type __pos = 0) const;
void swap(basic_string& __s) _GLIBCXX_NOEXCEPT;
const _CharT*
c_str() const _GLIBCXX_NOEXCEPT
{ return _M_data(); }
const _CharT*
data() const _GLIBCXX_NOEXCEPT
{ return _M_data(); }
size_type
find(const _CharT* __s, size_type __pos, size_type __n) const
_GLIBCXX_NOEXCEPT;
size_type
find_first_of(const basic_string& __str, size_type __pos = 0) const
_GLIBCXX_NOEXCEPT
{ return this->find_first_of(__str.data(), __pos, __str.size()); }
size_type
find_last_of(const basic_string& __str, size_type __pos = npos) const
_GLIBCXX_NOEXCEPT
{ return this->find_last_of(__str.data(), __pos, __str.size()); }
basic_string
substr(size_type __pos = 0, size_type __n = npos) const
{ return basic_string(*this,
_M_check(__pos, "basic_string::substr"), __n);
}
int compare(const basic_string& __str) const
{
const size_type __size = this->size();
const size_type __osize = __str.size();
const size_type __len = std::min(__size, __osize);
int __r = traits_type::compare(_M_data(), __str.data(), __len);
if (!__r)
__r = _S_compare(__size, __osize);
return __r;
}
最后看一下to_string的实现
inline string to_string(int __val) {
return __gnu_cxx::__to_xstring<string>(&std::vsnprintf, 4 * sizeof(int), "%d", __val);
}
inline string to_string(unsigned __val) {
return __gnu_cxx::__to_xstring<string>(&std::vsnprintf, 4 * sizeof(unsigned), "%u", __val);
}
inline string to_string(long __val) {
return __gnu_cxx::__to_xstring<string>(&std::vsnprintf, 4 * sizeof(long), "%ld", __val);
}
template <typename _String, typename _CharT = typename _String::value_type>
_String __to_xstring(int (*__convf)(_CharT*, std::size_t, const _CharT*, __builtin_va_list), std::size_t __n,
const _CharT* __fmt, ...) {
// XXX Eventually the result should be constructed in-place in
// the __cxx11 string, likely with the help of internal hooks.
_CharT* __s = static_cast<_CharT*>(__builtin_alloca(sizeof(_CharT) * __n));
__builtin_va_list __args;
__builtin_va_start(__args, __fmt);
const int __len = __convf(__s, __n, __fmt, __args);
__builtin_va_end(__args);
return _String(__s, __s + __len);
}
2.string底层函数详解
首先是_M_construct函数,一共有三个版本,我们只对第一个进行分析,后面了和第一个差不多一样。
template<typename _CharT, typename _Traits, typename _Alloc>
template<typename _InIterator>
void
basic_string<_CharT, _Traits, _Alloc>::
_M_construct(_InIterator __beg, _InIterator __end
, std::input_iterator_tag)
{
size_type __len = 0;
size_type __capacity = size_type(_S_local_capacity);
// 当长度小于默认的长度时,直接依次放进去
while (__beg != __end && __len < __capacity)
{
_M_data()[__len++] = *__beg;
++__beg;
}
__try
{
while (__beg != __end)
{
if (__len == __capacity)
{
// Allocate more space.
/**
* 就是在这里,当string内capacity不够容纳len个字符时,会使用_M_create去扩容
* 这里你可能会有疑惑,貌似每次while循环都会去重新使用_M_create来申请多一个字节的内存
* 但其实不是,_M_create的第一个参数的传递方式是引用传递,__capacity会在内部被修改,稍后会分析
*/
__capacity = __len + 1;
pointer __another = _M_create(__capacity, __len);
/**
* 把旧数据拷贝到新的内存区域,_M_data()指向的是旧数据,__another指向的是新申请的内存
*/
this->_S_copy(__another, _M_data(), __len);
/**
* __M_dispose()
* 释放_M_data()指向的旧数据内存,如果是_M_local_buf则不需要释放,稍后分析
*/
_M_dispose();
/**
* _M_data()
* 内部的指向内存的指针指向这块新申请的内存__another,它的实现其实就是
* void _M_data(pointer __p) { _M_dataplus._M_p = __p; }
*/
_M_data(__another);
// 更新_S_local_capacity
_M_capacity(__capacity);
}
_M_data()[__len++] = *__beg;
++__beg;
}
}
__catch(...)
{
/**
* 异常发生时,避免内存泄漏,会释放掉内部申请的内存
*/
_M_dispose();
__throw_exception_again;
}
// 最后更新string的长度__len
_M_set_length(__len);
}
template<typename _CharT, typename _Traits, typename _Alloc>
template<typename _InIterator>
void
basic_string<_CharT, _Traits, _Alloc>::
_M_construct(_InIterator __beg, _InIterator __end
, std::forward_iterator_tag)
{
// NB: Not required, but considered best practice.
if (__gnu_cxx::__is_null_pointer(__beg) && __beg != __end)
std::__throw_logic_error(__N("basic_string::"
"_M_construct null not valid"));
size_type __dnew = static_cast<size_type>(std::distance(__beg, __end));
if (__dnew > size_type(_S_local_capacity))
{
_M_data(_M_create(__dnew, size_type(0)));
_M_capacity(__dnew);
}
// Check for out_of_range and length_error exceptions.
__try
{ this->_S_copy_chars(_M_data(), __beg, __end); }
__catch(...)
{
_M_dispose();
__throw_exception_again;
}
_M_set_length(__dnew);
}
template<typename _CharT, typename _Traits, typename _Alloc>
void
basic_string<_CharT, _Traits, _Alloc>::
_M_construct(size_type __n, _CharT __c)
{
if (__n > size_type(_S_local_capacity))
{
_M_data(_M_create(__n, size_type(0)));
_M_capacity(__n);
}
if (__n)
this->_S_assign(_M_data(), __n, __c);
_M_set_length(__n);
}
_M_create函数的源代码分析如下,该函数的作用是进行内部内存的配置:
/**
* @brief _M_create表示申请新内存
* @param __capacity 想要申请的内存大小,注意这里参数传递方式是引用传递,内部会改变其值
* @param __old_capacity 以前的内存大小
*/
template <typename _CharT, typename _Traits, typename _Alloc>
typename basic_string<_CharT, _Traits, _Alloc>::pointer
basic_string<_CharT, _Traits, _Alloc>::_M_create(
size_type& __capacity, size_type __old_capacity) {
/**
* max_size()表示标准库容器规定的一次性可以分配到最大内存大小
* 当想要申请的内存大小最大规定长度时,会抛出异常
*/
if (__capacity > max_size()) std::__throw_length_error(__N("basic_string::_M_create"));
/**
* 这里就是常见的STL动态扩容机制,其实常见的就是申请为__old_capacity的2倍大小的内存,最大只能申请max_size()
* 注释只是说了常见的内存分配大小思想,不全是下面代码的意思,具体可以直接看下面这几行代码哈
*/
if (__capacity > __old_capacity && __capacity < 2 * __old_capacity)
{
__capacity = 2 * __old_capacity;
// Never allocate a string bigger than max_size.
if (__capacity > max_size()) __capacity = max_size();
}
/**
* 使用内存分配子去分配__capacity+1大小的内存,+1是为了多存储个\0
*/
return _Alloc_traits::allocate(_M_get_allocator(), __capacity + 1);
}
关于std::string的分析就到这里,前面为了让您看源码看的更清晰,对代码添加了详细的注释,同时做了适当的删减,但一定是正确的源代码,大家可放心阅读。相信您看完上面的源码分析可以回答出文章开头那几个问题并有所收获。
以上是关于STL string源码分析的主要内容,如果未能解决你的问题,请参考以下文章
STL源码拆解基于源码分析forward_lsit容器实现(详细!)
STL源码拆解基于源码分析forward_lsit容器实现(详细!)