C++标准库STL中的字符串string是基本类型,标准进化过程中对string的最终接口和行为方式有过变化和调整,主要的是C++11要求string不能使用COW(Copy on Write)机制。C++11前很多STL实现(以HP STL为基础衍生)使用的是引用计数支持的写时拷贝方式,即所谓COW,C++11后的基本使用独立堆空间读写各自内存。
这个主要变化前后,string复制和构造的方式,执行过程中着重点,资源和效率都有不同,在源码实现分析下这些变化,可总结出使用string各程序环境的功能实施和效率影响。
代码分析以g++采用的SGI STL为例,gcc支持C++11在版本4.8.5,不同的实现在这个版本前后。
这两个实现主要方式:
C++11前的写时复制机制主要是通过引用计数的方法,把字符串内容相同的构造和赋值全部指向同一个实际数据空间,并通过记录引用数决定数据的创建和销毁,在共享同一个数据的几个string中有一个需要改变内容时,再拷贝一份数据由该string指向并修改内容。
C++11后独立空间的string一般由局部和堆两个空间配合,局部固定长度的空间存放短字符串,堆空间在数据超出局部空间时分配,多数一般的短字符串只使用局部空间读写,减少了大量动态堆内存分配/销毁。构造和修改时string作用自己的局部或堆空间。
1. C++11后独立空间string
C++11后string通过定义固定的小数组和堆指针,实现长短配合独立空间。多数的小字符串直接使用固定数组,实际空间对局部string在栈上,全局string在全局数据区,这样常规字符串和频繁使用的临时字符串不需要动态内存参与;字符串内容较多的长串出现时分配适当的堆内存(一般在内容的1倍到2倍之间),应对这种较少的情况。
这里有个标准升级后的相应问题,关于ABI(Application Binary Interface),应用程序二进制接口,即一种较低层的接口,是程序的二进制文件被别的二进制程序调用时约定的接口,涉及到函数调用、寄存器、栈形状等,可以理解为C++11对编译后程序的二进制调用方式,有新的标准定义或约定,这些方式包括程序调用库、程序调用其自身、库调用库的函数和数据的接口。C++11的string变化有些接口功能要求执行方式和二进制ABI相关并通过ABI表现出来,所以STL库C++11通过ABI版本判断同时条件编译决定了string的实现形式。SGI STL定义宏_GLIBCXX_USE_CXX11_ABI,并用这个宏标识string实现是C++11前还是后,打开这个宏,string是C++11后要求的独立空间方式。
这个定义的简要形式是:
#if _GLIBCXX_USE_CXX11_ABI
namespace std {
class string {/*独立空间*/};
};
#else
namespace std {
class string {/*写时拷贝*/};
}
#endif
C++11后独立空间string实现在条件编译定义_GLIBCXX_USE_CXX11_ABI=1时
实现分析代码是gcc 8.1.0版本,string的定义在4.8.5后基本上变化不大
string是模板类basic_string的一个特化,其中字符类型是char,char_traits是类型萃取器,实现一些字符相关性质判断,allocator是使用基本new和delete的空间分配器
template<typename _CharT, typename _Traits = char_traits<_CharT>,
typename _Alloc = allocator<_CharT> >
class basic_string;
typedef basic_string<char> string;
长短字符空间配合的方式实现基础是通过数据结构定义,这些是定义在basic_string中的private成员
struct _Alloc_hider : allocator_type {
#if __cplusplus < 201103L
...
#else
_Alloc_hider(pointer __dat, const _Alloc& __a)
: allocator_type(__a), _M_p(__dat) { }
_Alloc_hider(pointer __dat, _Alloc&& __a = _Alloc())
: allocator_type(std::move(__a)), _M_p(__dat) { }
#endif
pointer _M_p; // The actual data.
};
_Alloc_hider _M_dataplus;
size_type _M_string_length;
enum { _S_local_capacity = 15 / sizeof(_CharT) };
union
{
_CharT _M_local_buf[_S_local_capacity + 1];
size_type _M_allocated_capacity;
};
_Alloc_hider是实现了分配功能的实际数据的结构类型,_M_p是数据的指针,继承的allocator可以对数据分配/销毁。定义的_M_dataplus是_Alloc_hider类型,_M_string_length是字符串长度,不含结尾0
联合定义局部短字符空间_M_local_buf,这个变量同时可以在使用堆空间时存储空间容量_M_allocated_capacity,可以看到局部空间的固定长度16个字节,字符容量最长15个
_Alloc_hider的构造函数定义复制和移动分配器的方法,_M_p指向实际数据,使用局部空间时等于_M_local_buf地址,使用堆空间时由_Alloc_hider继承的分配器分配
独立空间的string构造函数定义了一组不同参数形式,其中主要的几个:
basic_string()
: _M_dataplus(_M_local_data())
{ _M_set_length(0); }
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()); }
#if __cplusplus >= 201103L
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);
}
#endif // C++11
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); }
basic_string(size_type __n, _CharT __c, const _Alloc& __a = _Alloc())
: _M_dataplus(_M_local_data(), __a)
{ _M_construct(__n, __c); }
#if __cplusplus >= 201103L
template<typename _InputIterator, typename = std::_RequireInputIter<_InputIterator>>
#else
...
#endif
basic_string(_InputIterator __beg, _InputIterator __end, const _Alloc& __a = _Alloc())
: _M_dataplus(_M_local_data(), __a)
{ _M_construct(__beg, __end); }
构造几个主要类型,默认构造、复制构造、移动构造、C字符串构造、重复字符构造、迭代器构造,这些构造默认的数据指针初始指向局部空间,然后执行相应的构造动作,其中_M_local_data返回局部空间指针,_M_data返回实际数据指针,_M_construct执行构造动作,这几个函数实现:
pointer
_M_local_data()
{
#if __cplusplus >= 201103L
//指针类型traits建立指针函数,相当于取地址&
return std::pointer_traits<pointer>::pointer_to(*_M_local_buf);
#else
return pointer(_M_local_buf); //转成指针类型,pointer是字符指针
#endif
}
pointer
_M_data() const
{ return _M_dataplus._M_p; }
bool
_M_is_local() const
{ return _M_data() == _M_local_data(); }
template<typename _InIterator>
void
_M_construct(_InIterator __beg, _InIterator __end)
{
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);
}
void
_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); //重复复制n个字符
_M_set_length(__n);
}
pointer
_M_create(size_type& __capacity, size_type __old_capacity)
{
if (__capacity > max_size())
std::__throw_length_error(__N("basic_string::_M_create"));
// 每次空间不足时分配
// 分配策略最少现有空间2倍,避免分配空间空闲太少频繁分配
if (__capacity > __old_capacity && __capacity < 2 * __old_capacity)
{
__capacity = 2 * __old_capacity;
if (__capacity > max_size())
__capacity = max_size();
}
// 结尾0占用一个空间
return _Alloc_traits::allocate(_M_get_allocator(), __capacity + 1);
}
void
_M_capacity(size_type __capacity)
{ _M_allocated_capacity = __capacity; }
template<class _Iterator>
static void
_S_copy_chars(_CharT* __p, _Iterator __k1, _Iterator __k2)
{
for (; __k1 != __k2; ++__k1, (void)++__p)
traits_type::assign(*__p, *__k1); // 单个字符类型赋值
}
void
_M_dispose() //弃置字符串,使用局部空间不需动作,使用堆空间销毁
{
if (!_M_is_local())
_M_destroy(_M_allocated_capacity);
}
void
_M_set_length(size_type __n) //设置字符串长度,并赋值结尾0
{
_M_length(__n);
traits_type::assign(_M_data()[__n], _CharT());
}
_M_construct首先按构造字符长度需要确定使用局部空间或调用_M_create分配堆空间,然后按参数执行逐个复制或重复复制。_M_create是对内存分配函数,构造和字符长度增长到空间不足时调用,重分配策略是至少现有空间2倍,需要在2倍和最大值之间按需要,避免扩容过小频繁分配
basic_string的几个构造函数的实际动作:
- 默认构造 数据使用局部空间并清空字符串
- 复制构造 数据初始指向局部空间,分配器复制构造同__str,执行构造动作,即按__str长度选择是否分配堆空间,复制__str内容
- 移动构造 数据初始指向局部空间,分配器移动构造同__str,如果__str数据在局部,复制到this局部,__str数据在堆,直接复制指针,清空__str为局部空字符
- C字符串构造 同复制构造过程,执行构造的迭代器换成C字符指针
- 重复字符构造 同复制构造过程,执行构造动作重复复制
- 迭代器构造 同复制构造过程
独立空间string赋值操作执行也是局部和堆空间配合,主要赋值操作符:
basic_string&
operator=(const basic_string& __str)
{
#if __cplusplus >= 201103L
if (_Alloc_traits::_S_propagate_on_copy_assign())
{
if (!_Alloc_traits::_S_always_equal() && !_M_is_local()
&& _M_get_allocator() != __str._M_get_allocator())
{
// Propagating allocator cannot free existing storage so must
// deallocate it before replacing current allocator.
if (__str.size() <= _S_local_capacity)
{
_M_destroy(_M_allocated_capacity);
_M_data(_M_local_data());
_M_set_length(0);
}
else
{
const auto __len = __str.size();
auto __alloc = __str._M_get_allocator();
// If this allocation throws there are no effects:
auto __ptr = _Alloc_traits::allocate(__alloc, __len + 1);
_M_destroy(_M_allocated_capacity);
_M_data(__ptr);
_M_capacity(__len);
_M_set_length(__len);
}
}
std::__alloc_on_copy(_M_get_allocator(), __str._M_get_allocator());
}
#endif
return this->assign(__str);
}
#if __cplusplus >= 201103L
basic_string&
operator=(basic_string&& __str)
{
if (!_M_is_local() && _Alloc_traits::_S_propagate_on_move_assign()
&& !_Alloc_traits::_S_always_equal()
&& _M_get_allocator() != __str._M_get_allocator())
{
// Destroy existing storage before replacing allocator.
_M_destroy(_M_allocated_capacity);
_M_data(_M_local_data());
_M_set_length(0);
}
// Replace allocator if POCMA is true.
std::__alloc_on_move(_M_get_allocator(), __str._M_get_allocator());
if (!__str._M_is_local() && (_Alloc_traits::_S_propagate_on_move_assign()
|| _Alloc_traits::_S_always_equal()))
{
pointer __data = nullptr;
size_type __capacity;
if (!_M_is_local())
{
if (_Alloc_traits::_S_always_equal())
{
__data = _M_data();
__capacity = _M_allocated_capacity;
}
else
_M_destroy(_M_allocated_capacity);
}
_M_data(__str._M_data());
_M_length(__str.length());
_M_capacity(__str._M_allocated_capacity);
if (__data)
{
__str._M_data(__data);
__str._M_capacity(__capacity);
}
else
__str._M_data(__str._M_local_buf);
}
else
assign(__str);
__str.clear();
return *this;
}
basic_string&
operator=(const _CharT* __s)
{ return this->assign(__s); }
可以看出,赋值操作和构造过程基本相似,只是少了初值指向局部,且对当前值和分配条件作了不同处理
复制赋值 判断了当前值和分配器
- __str的分配器和当前分配器可互拷贝但不相同转2,否则直接复制__str内容,其中__str长度大于当前容量重分配堆内存;
- 如果当前使用堆内存,则销毁当前堆空间并数据指向局部空间,使用局部空间则不变;
- 如果__str长度小于局部空间长度,复制__str内容,__str大于局部空间,用__str的分配器分配堆内存,复制__str内容;
复制过程总结即调整局部和堆空间符合待赋__str恰当状态,逐字符复制
移动赋值 同样判断当前值和分配器,三种情况
- 当前使用堆空间且分配器不同,销毁当前堆空间并指向局部空间,如果__str使用堆空间,复制__str堆指针并置0,__str使用局部空间,逐字符复制
- 当前使用堆空间且分配器相同,如果__str使用堆空间,交换当前和__str的堆指针,__str使用局部空间,逐字符复制
- 当前使用局部空间,直接复制__str内容,其中__str长度大于当前容量重分配堆内存
最后清空待赋__str
- C字符串赋值 直接复制,待赋长度大于当前容量重分配堆内存
总结
C++11后独立空间的string,执行和分配逻辑比较简单,string持有自己的内存空间,在短字符和长字符间作了策略处理,使移动构造部分情况也需逐字符复制,但是减少了常用临时的短字符动态内存分配,且复制短字符效率影响较小,故该策略作出了效率取舍,同时减少系统内存碎片,分配器和复制的底层操作也有优化。
2. C++11前写时复制string
C++11前由于STL标准没有规定,很多string实现版本用了一种效率和空间都有提升的策略,基于共享堆内存引用计数,修改时分配独立空间,即所谓写时复制COW。这种方式对字符串只分配一块堆内存,内存中存储引用计数和数据长度等变量,变量后空间放字符数据。构造和赋值string时,只对原字符串引用计数加1,新字符串指向同一堆内存。销毁string时,对指向的堆内存中引用计数减1,如果没有指向堆内存的引用销毁堆内存,否则有引用则保持堆内存。
引用计数的数据结构通过_Rep结构体实现
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
struct _Rep : _Rep_base
{
static const size_type _S_max_size;
static const _CharT _S_terminal;
static size_type _S_empty_rep_storage[];
static _Rep&
_S_empty_rep() _GLIBCXX_NOEXCEPT
{
//返回空字符串,静态成员_Rep_base数据全0,表示空字符
void* __p = reinterpret_cast<void*>(&_S_empty_rep_storage);
return *reinterpret_cast<_Rep*>(__p);
}
bool
_M_is_leaked() const _GLIBCXX_NOEXCEPT
{ return this->_M_refcount < 0; }
bool
_M_is_shared() const _GLIBCXX_NOEXCEPT
{ return this->_M_refcount > 0; }
void
_M_set_leaked() _GLIBCXX_NOEXCEPT
{ this->_M_refcount = -1; }
void
_M_set_sharable() _GLIBCXX_NOEXCEPT
{ this->_M_refcount = 0; }
void
_M_set_length_and_sharable(size_type __n) _GLIBCXX_NOEXCEPT
{
//设置引用计数1次并设置长度和结尾0
this->_M_set_sharable(); // One reference.
this->_M_length = __n;
traits_type::assign(this->_M_refdata()[__n], _S_terminal);
}
_CharT*
_M_refdata() throw()
{ return reinterpret_cast<_CharT*>(this + 1); }
_CharT*
_M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2)
{
return (!_M_is_leaked() && __alloc1 == __alloc2)
? _M_refcopy() : _M_clone(__alloc1);
}
_CharT*
_M_refcopy() throw()
{
__gnu_cxx::__atomic_add_dispatch(&this->_M_refcount, 1);
return _M_refdata();
}
void
_M_dispose(const _Alloc& __a) _GLIBCXX_NOEXCEPT
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&this->_M_refcount);
if (__gnu_cxx::__exchange_and_add_dispatch(&this->_M_refcount, -1) <= 0)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&this->_M_refcount);
_M_destroy(__a);
}
}
_Rep_base中三个成员是引用计数使用的变量,_M_refcount是引用计数值,值代表3种状态,-1是非正常状态1次引用,0是正常状态1次引用,n>0是n+1次引用;_M_length是字符串长度;_M_capacity是数据空间容量。长度不算结尾0,字符串实际占用空间_M_length+1,空间实际容量_M_capacity+1。这三个变量全0表示空字符串,这后面有一个字节0表示空字符串数据,_Rep的静态成员_S_empty_rep_storage表示一个空字符串实例,即一个_Rep型数据全0结构体空间,转成_Rep型指针可用于空字符比较判断。
_S_max_size和_S_terminal是常数为字符最大长度和结尾0。
其中几个设置和判断状态的函数,_M_is_leaked()是否非正常状态,_M_set_leaked()设置非正常,_M_is_shared()是否2个及以上引用,_M_set_sharable()设置1个引用正常状态,_M_set_length_and_sharable(size_type __n)设置长度和1个引用正常状态。
引用计数的维护是_M_refcopy()和M_dispose(const _Alloc& __a)函数,分别表示引用拷贝即加1和引用销毁即减1,其中_M_refcount加减用了原子方法,基于_M_refcount声明为原子类型,计数减1后如果引用数小于等于-1即没有引用,销毁堆空间。
最后是引用数据判断和访问函数_M_refdata()和_M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2),这需要结合后面空间分配结构查看
堆空间分配和销毁函数:
static _Rep*
_S_create(size_type __capacity, size_type __old_capacity, const _Alloc& __alloc)
{
if (__capacity > __old_capacity && __capacity < 2 * __old_capacity)
__capacity = 2 * __old_capacity;
size_type __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep);
void* __place = _Raw_bytes_alloc(__alloc).allocate(__size);
_Rep *__p = new (__place) _Rep;
__p->_M_capacity = __capacity;
__p->_M_set_sharable();
return __p;
}
void
_M_destroy(const _Alloc& __a) throw ()
{
const size_type __size = sizeof(_Rep_base) + (this->_M_capacity + 1) * sizeof(_CharT);
_Raw_bytes_alloc(__a).deallocate(reinterpret_cast<char*>(this), __size);
}
空间分配同样使用最小现有2倍的策略,这中间有部分调整页面相关策略针对具体malloc实现的多数操作系统设置最小值引起无效扩容,与此主题无关忽略了。可以看出,堆空间大小是数据空间+1结尾0+_Rep结构,再看_M_refdata()函数返回数据空间地址在一个_Rep结构之后,可确定堆空间内存布局是头部_Rep结构后面数据空间。_M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2)是返回一个当前字符串的__alloc1分配器可用的拷贝,如果当前字符串可引用计数则返回引用拷贝(引用计数加1的当前字符串),不可引用则返回一个__alloc1分配空间并复制内容的独立拷贝。
_M_destroy(const _Alloc& __a)即正常用分配器销毁空间。
写时拷贝策略实现通过_M_replace_safe、_M_mutate几个函数,一个典型例子赋值函数assign:
basic_string&
assign(const _CharT* __s, size_type __n)
{
if (_M_disjunct(__s) || _M_rep()->_M_is_shared())
return _M_replace_safe(size_type(0), this->size(), __s, __n);
else
{
// Work in-place.
const size_type __pos = __s - _M_data();
if (__pos >= __n)
_M_copy(_M_data(), __s, __n);
else if (__pos)
_M_move(_M_data(), __s, __n);
_M_rep()->_M_set_length_and_sharable(__n);
return *this;
}
}
basic_string&
_M_replace_safe(size_type __pos1, size_type __n1, const _CharT* __s, size_type __n2)
{
_M_mutate(__pos1, __n1, __n2);
if (__n2)
_M_copy(_M_data() + __pos1, __s, __n2);
return *this;
}
void
_M_mutate(size_type __pos, size_type __len1, size_type __len2)
{
const size_type __old_size = this->size();
const size_type __new_size = __old_size + __len2 - __len1;
const size_type __how_much = __old_size - __pos - __len1;
if (__new_size > this->capacity() || _M_rep()->_M_is_shared())
{
// Must reallocate.
const allocator_type __a = get_allocator();
_Rep* __r = _Rep::_S_create(__new_size, this->capacity(), __a);
if (__pos)
_M_copy(__r->_M_refdata(), _M_data(), __pos);
if (__how_much)
_M_copy(__r->_M_refdata() + __pos + __len2,
_M_data() + __pos + __len1, __how_much);
_M_rep()->_M_dispose(__a);
_M_data(__r->_M_refdata());
}
else if (__how_much && __len1 != __len2)
{
// Work in-place.
_M_move(_M_data() + __pos + __len2,
_M_data() + __pos + __len1, __how_much);
}
_M_rep()->_M_set_length_and_sharable(__new_size);
}
其中assign的写时拷贝发生在if分支当前字符串和__s空间不重叠_M_disjunct或有共享引用_M_is_shared,执行_M_replace_safe。执行空间调整_M_mutate,分两种情况,如果没有共享引用,需要赋值的字符串超过当前容量则重新分配堆空间,不超过直接返回执行赋值;如果有共享引用,新分配独立堆空间并复制源内容,这里即实现了写时拷贝策略,返回执行赋值。赋值是逐字符复制_M_copy。
写时拷贝策略string的构造函数,主要的几个:
basic_string()
: _M_dataplus(_S_construct(size_type(), _CharT(), _Alloc()), _Alloc()){ }
basic_string(const basic_string& __str)
: _M_dataplus(__str._M_rep()->_M_grab(_Alloc(__str.get_allocator()), __str.get_allocator()),
__str.get_allocator()){ }
basic_string(basic_string&& __str)
: _M_dataplus(__str._M_dataplus)
{ __str._M_data(_S_construct(size_type(), _CharT(), get_allocator())); }
static _CharT*
_S_construct(size_type __n, _CharT __c, const _Alloc& __a)
{
// Check for out_of_range and length_error exceptions.
_Rep* __r = _Rep::_S_create(__n, size_type(0), __a);
if (__n)
_M_assign(__r->_M_refdata(), __n, __c);
__r->_M_set_length_and_sharable(__n);
return __r->_M_refdata();
}
构造分类中典型的是默认构造、复制构造、移动构造,主要的是涉及应用引用计数,复制时状态判断,重复字符分配空间问题,构造时的实际动作:
- 默认构造 直接分配了无空间空内容引用1的数据
- 复制构造 对待复制_str分情况处理,如果_str可引用计数则计数加1,不可计数分配新空间复制_str
- 移动构造 对_str包括引用计数和长度的整个堆空间进行移动,并再分配空字符1引用给_str
_S_construct函数分配引用计数堆空间,长度_n,引用数1,并重复_c字符_n次赋值
写时拷贝的string构造的特点是复制只是引用计数增加,不复制
赋值操作符定义主要的几个:
basic_string&
operator=(const basic_string& __str)
{ return this->assign(__str); }
basic_string&
operator=(const _CharT* __s)
{ return this->assign(__s); }
basic_string&
operator=(basic_string&& __str)
{
this->swap(__str);
return *this;
}
赋值操作符执行的是写时拷贝的策略,调用的assign函数前面分析了,移动赋值同样针对整个引用计数的堆空间,执行的实际动作:
- 复制赋值 执行assign函数,实施写时拷贝策略
- C字符串赋值 同样执行assign
- 移动赋值 对整个引用计数堆空间,如果_str和当前字符串分配器相同,直接交换堆空间,不相同则分别用对应分配器分配新空间,交换赋值内容
总结
C++11前的写时拷贝策略的string特点是一整个堆内存存储引用计数、长度、容量等变量和数据内容,对同内容的字符构造共享已有堆内存仅增加引用计数,一定程度节省内存空间。但是构造、赋值和销毁时对引用计数处理执行的运行判断步骤也有一定成本,只是小于堆内存分配,还面临一个问题是共享的引用计数堆空间在多线程中需要数据同步,即使实际写时拷贝的都是只有一个引用的独立空间
3. 实质动作和效率影响对使用string的考量
C++11标准的修改对string的实现产生了基础策略层面的变化,导致行为影响作用不同,使用时需要对各种不同情况分析实质动作,对使用方式的正确和效率准确判定。
C++11后独立空间的string数据空间字符串自己持有,但设计了长短空间区分局部和堆内存,这时局部变量和类成员变量的字符串频繁使用临时构造销毁时,减少了堆内存分配次数,同时全部局部独立复制不需数据同步,正确可以预判,且效率较高。但是这种字符串需要传递出作用域时,不论是构造新字符串或赋值给其他字符串都会逐字符复制,因此应尽可能使用移动构造或赋值。
C++11前引用计数的写时拷贝string,充分利用相同字符和内容不变的状态,使用类似智能指针的共享引用计数策略,在需要改变的最后一刻复制字符串,这样的策略节省了空间,不论临时局部字符串或传递进出作用域,传递时使用复制或移动方式,在内容不变时都没有效率的额外较大开销,缺点也很明显,内容变动充分的情况会持续堆内存分配,不论内容长短。
使用是典型场景示例分析:
void temp_string(string origin){
//temp是局部串
//C++11后如果是短字符使用局部空间提倡这样写,如果长字符直接用origin
//C++11前引用计数如果后面using中不改变这样写,如果改变建议直接append确定分配空间
string temp = origin;
//another同temp,只是构造换为赋值
string another;
another = origin;
//using temp and another
...
//end using
if(origin == temp){ //局部串msg调用移动构造,这样写或直接用origin + " equal"差别不大
string msg = origin + " equal";
cout<<msg<<endl;
} else {
string msg = origin + " not equal";
cout<<msg<<endl;
}
}
string global;
void using_string(string arg){ //值传递,C++11前引用计数使用
void using_string(const string &arg){ //左值引用,C++11前同值传递差别不大,C++11后使用不需复制
void using_string(string &&arg){ //右值引用,C++11前后都是移动,只是C++11前不销毁只是引用计数减1
cout<<arg<<endl;
}
void pass_string(){
string temp;
temp = "temp to global";
global = temp; //C++11前引用计数传递
global = move(temp); //C++11后移动赋值
using_string(temp); //函数定义原则见实现
vector<string> vec;
string obj;
vec.push_back(obj); //C++11前引用计数构造
vec.push_back(move(obj)); //C++11后移动构造
vec.emplace_back(obj); //C++11后转发传递,复制构造
vec.emplace_back(move(obj)); //C++11后转发传递,移动构造
}
string return_string(){
string local = "local return";
//这里不涉及string实现,关系编译优化RVO原则
//返回局部变量,如果编译器优化打开且支持,优化等级不同可能以下三种行为
//在调用函数时返回赋值的变量直接构造"local return"
//构造一次local,再构造一次返回赋值的变量
//构造一次local,构造一个临时返回变量,再构造一次返回赋值的变量
return local;
//如果编译器不支持RVO或不打开优化,直接使用move后返回局部变量,出现以下两种行为的一种
//构造一次local,再移动给返回赋值的变量
//构造一次local,移动给一个临时返回变量,再移动给返回赋值的变量
return move(local);
}
int main(){
string origin = "origin";
temp_string(origin); //局部字符串行为和使用建议见定义
pass_string(); //字符串传递出作用域行为和使用建议见定义
string ret_rvo = return_string(); //返回值RVO优化见定义
}