C++ string类的隐式共享写时拷贝的实现及设计要点
作者:网络转载 发布时间:[ 2016/12/8 11:06:31 ] 推荐标签:C++ String
8)重载+=操作符,实现字符串连接
String& String::operator+=(const String &s)
{
_addAssignOpt(s._cstr, s._length);
return *this;
}
String& String::operator+=(const char *cstr)
{
if(cstr != NULL)
_addAssignOpt(cstr, strlen(cstr));
return *this;
}
void String::_addAssignOpt(const char *cstr, size_t len)
{
if(*_used == 1)
_addString(cstr, len);
else
{
_decUsed();
_cstr = _renewAndCat(cstr, len);
_used = new size_t(1);
}
}
void String::_addString(const char *cstr, size_t len)
{
//本函数,只有在引用计数为1时,才可用
if(*_used != 1)
return;
if(len + _length > _capacity)
{
char *ptr = _renewAndCat(cstr, len);
delete[] _cstr;
_cstr = ptr;
}
else
{
strncat(_cstr, cstr, len);
_length += len;
}
}
char* String::_renewAndCat(const char *cstr, size_t len)
{
size_t new_len = len + _length;
size_t capacity = new_len;
capacity += (capacity >> 1);
char *ptr = new char[capacity+1];
if(_cstr != NULL)
memcpy(ptr, _cstr, _length);
ptr[_length] = 0;
_length = new_len;
_capacity = capacity;
strncat(ptr, cstr, len);
return ptr;
}
+=是一个复杂的操作,也是我实现时琢磨得久的操作。因为它是写的操作,根据写时拷贝的原则,它需要减少其原先字符数组的引用计数,同时创建一个新的字符数组来储存增加长度后的字符串。并且该对象所引用的字符数组可能只有该对象自己在引用,也是说其引用计数为1,此时减少其引用计数还可能导致原先字符数组的释放,从而丢失数据,并在使用指向原先字符数组的指针进行数据复制时发生错误。所以引用计数是否为1应该采用不同的策略。
而当引用计数为1时,我们可以认为该对象独立享有该字符数组,可以对其进行任何操作而不影响其他对象,这时,我们可以把字符串直接追加到已经的字符数组的后面,而这样做可能因为字符数组的容量不够而不能进行,这时为字符数组的重新分配合适的空间。
当引用计数不为1时,我们首先调用_decUsed()来减少原字符数组的引用计数,然后调用_renewAndCat来连接并产生新的字符数组,然后重置_cstr的指向,并new一个新的引用计数,初始值置为1.
上面的函数中,_renewAndCat的功能是分配新的字符数组,同时把原先的字符数据复制到新的字符数组中,再在新的字符数组中追加字符串,返回新的字符数组的首地址。_addString是只有当引用计数为1时才能调用的函数,其字符数组足以容纳连接后的字符串,则直接连接,若不能,则调用_renewAndCat重新分配合适的字符数组,并进行复制,后,把旧的字符数组delete[]掉,再把_cstr赋值为_renewAndCat创建的新字符数组的首地址。
注:本人认为,如果一个字符串对象做了一次+=运算,那么它很可能会很快做第二次,所以在分配内存时,我采用了预分配的策略,每次分配都分配连接完成后的字符串的长度的1.5倍。这样当下一次执行+=时,字符数组可能有足够多的容量来保存连接后的字符串,而不用重新分配和复制。而且我们注意到,当调用+=一次之后,*_used肯定为1,即下次运行+=时,是极有可能直接加到字符数组的后面的。
为了提高程序的运行效率,在进行1.5倍的预分配时,没有使用浮点数乘法,更没有使用除法,而是采用了移位运算,如下:
int capacity = len;
capacity += (capacity >> 1)
其对应的数学表达式为:“x = capacity + capacity/2;capacity = x”。因为右移运算相当于除以2,这样实现了乘以1.5的运算操作。
9)清空字符串
void String::clear()
{
_decUsed();
_cstr = NULL;
_used = new size_t(1);
_length = 0;
_capacity = 0;
}
该函数用于清除字符串对象引用的字符数组的数据,所以我们只需要调用_decUsed函数,减少对象所引用的字符数组的引用计数,并把其他成员变量设置为默认的值即可。即与默认构造函数所设置的值一致。
注:以下函数不是String的成员函数
10)重载+操作符
String operator +(const String &lhs, const String &rhs)
{
String stemp(lhs);
stemp += rhs;
return stemp;
}
该函数的实现可以借助上面实现的+=操作符,先用第一个对象rhs复制构造一个临时对象stemp,然后通过把第二个参数追加到临时对象stemp上,返回stemp即可简单轻松地实现+操作符的重载。
11)重载输出操作符
ostream& operator << (ostream &os, const String &s)
{
os<<s.cstr();
return os;
}
12)重载输入操作符
istream& operator >> (istream &in, String &s)
{
const int BUFFER_SIZE = 256;
char buffer[BUFFER_SIZE];
char *end = buffer + BUFFER_SIZE -1;
s.clear();
do
{
//用于判断是否读完输入内容,因为如果还未读取的输入字符数大于buffer
//的容量,则buffer的后一个字符会被get函数置为'