C++ string类的隐式共享写时拷贝的实现及设计要点(三)

2014-11-24 11:34:39 · 作者: · 浏览: 3
pacity; 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<
  
   
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函数置为'\0'
        *end = '#';
        in.get(buffer, BUFFER_SIZE);
        s += buffer;
    }while(*end == '\0');
    in.get();
    return in;
}
实现输入操作符的重载的一个困难之处就是我们不知道用户要输入的字符串的长度,也就不知道应该分配一个多大的缓冲区来接收输入的字符。所以在这里,设置一个一定大小的缓冲,采用循环读取,连续添加到字符串对象中的方法来实现。那么如何知道该循环读取输入多少次呢?也就是说,怎么知道已经把所有的输入字符读取完毕呢?在这里,我使用了一个标准输入流istream的get成员函数,该成员函数从输入流中读取指定个数的字符或遇到输入流结束而返回,注意最后它会自动加入一个空字符‘\0’作为结束。这个空字符也作为读入的字符数量的计数。例如,如果有一个大小为6的char型数组作为buffer,从标准输入流中读入6个字符,实际上只会从标准输入中读入最多5个字符(因为可能遇到流结束),并把空字符‘\0’加入到buffer的末尾。
所以我们可以把buffer的最后一个字节,设置成我们自己特定的一个字符(只是是非‘\0’即可),如这里的'#',然后读入buffer大小的字符数。若还没有读取完毕,我们设置的这个特殊的字符会被空字符'\0'覆盖,我们从而知道,还没读取完标准输入的数据。若我们设置的特殊字符没有被覆盖,就说明,读到的数据不足以填满buffer,也就是说,我们已经没有数据可读了,从而可以判断已经读取完所有输入的字符。
注:输入也是一个写的操作,并且会把对象之前的内容覆盖掉,所以在输入到对象之前,要先调用clear成员函数,把对象清空。
四、测试代码
#include 
    
     
#include "_stringv2.h"
using std::cin;
using std::cout;
using std::cin;
using std::endl;
int main()
{
    String s1;
    s1 = "abc";
    {
        String s2(s1);
        s2 += s1;
        cout << s2 << endl;
    }
    String s3(s1);
    cin >> s3;
    cout << s3 << endl;
    cout << s1 << endl;
    String s4 = s1 + s3;
    cout << s4 << endl;
}
    
运行结果如下: \
五、代码分析 首先定义一个String的对象s1,s1调用默认构造函数,生成一个默认的对象,然后调用赋值操作函数,为s1分配堆内存字符数组。对象s2是以对象s1的样本复制构造出来的对象,其作用域只在花括号内。我们可以看到s2的改变并没有影响到s1。其他的调用也一样,从而可以看到是实现了隐式共享,写时拷贝。从运行的结果可以看出,一切的运行都是没有问题的,与标准库的string的输出一致。