string的深浅拷贝以及写时拷贝问题

时间:2021-10-11 21:50:27

首先string的浅拷贝是让两个不同的指针指向同一块空间,而这在析构的时候会出现将一块空间释放两次,程序会崩溃,因此我们才需要进行深拷贝,即第二个指针开辟和第一个指针一样大小空间,然后将内容复制过去,不过深拷贝又分传统写法和现代写法,两者的区别主要在于拷贝构造和赋值运算符的重载上

第一种就是传统写法,拷贝构造和赋值的时候正常开辟空间,正常拷贝内容

class String
{
public:
    String()//这是先给pstr开辟一个空间来存放'\0',注意'\0'的写法
        :p(new char[1])
    {
        *p = '\0';\
    }
    //显示的给出 开辟空间时候加1是为了放'\0'
    //也可以合起来写 改成String(char*p="")
    String(char *pstr)
        :p(new char[strlen(pstr)+1])
    {
        if (pstr == NULL)
        {
            p = new char[1];
            *p = '\0';
        }
        else
        {
            p = new char[strlen(pstr) + 1];
            strcpy(p, pstr);
        }
    }
    //拷贝构造函数
    String(const String &s)
    {
        p = new char[strlen(s.p) + 1];
        strcpy(p, s.p);
    }

    //赋值运算符重载 注意四个问题
    //1)返回值
    //2)参数
    //3)检测其是否给自身赋值
    //4)返回为*this 
    String & operator = (const String &s)
    {
        if (this != &s)
        {
            char *tmp = new char[strlen(s.p) + 1];
            strcpy(tmp, s.p);
            delete []p;
            p = tmp;
        }
        return *this;
    }

//析构函数
    ~String()
    {
        if (p)
        {
            delete[] p;
        }
    }

private:
    char *p;
};

2 深拷贝的现代写法
现代写法构造函数和析构函数并没有改变,改变的是复制运算符重载,拷贝构造
先看这段实现代码,第一次我是这样写的,但是崩溃了。。。

String(const String &s)
      {
        String tmp(s.p);
        std::swap(p, tmp.p);
    }

//赋值运算符重载
String &operator=( String s)
    {
        std::swap(p, s.p);
        return *this;
    }

//另一种写法
    String & operator=(const String &s)
    {
        if ( this != &s)
        {
            String tmp(s.p);
            std::swap(p, tmp.p);
        }
        return *this;
    }

看起来没毛病啊,所以这个时候可以单步进去跟踪一下看看程序到底是在哪里崩溃的。单步执行就会看到在拷贝构造的时候会出现下面的图情况,我们没有将_str的地址初始化,即访问了非法地址,但是这个时候还不会崩溃,而在交换之后,将它交换给tmp对象,在出了作用域之后tmp对象在析构的时候就会崩溃,因为是在释放非法内存空间。
string的深浅拷贝以及写时拷贝问题
因此,我们必须在使用之前在初始化的列表里面将_str置空,切记千万不能忘记了。

写到这突然想起来在写日期类的时候我写了一个这样的代码,

Date& operator=(const Date& d)
    {
        if (this != &d)
        {
            Date tmp(d);
            std::swap(*this, tmp);
        }
        return *this;
    }

运行的时候会出现死循环,因为swap在交换的时候也是要创建临时变量,而自定义类型的创建又会调用拷贝构造,但是拷贝构造的时候有在创建临时变量,这样的话就会陷入无限的递归调用,最终栈溢出。

因此,在使用库函数swap进行交换的时候最好只去交换内置类型,对于自定义类型的类型,一定要自己去实现。

3.但是在有的时候我们并不需要每次都去开辟空间,拷贝数据,那么存不存在一种方法可以让我们一般情况下只实行浅拷贝,然后当我们需要改变新的空间的内容的时候,才会重新开辟空间呢?可以想到下面这些方法:
1)首先我们会想到增加一个类成员 int count,但是这样造成的后果是每一个成员都有一个不同的count 在析构的时候就很混乱还是会出错
2)然后呢我们会想到使用静态成员的办法,因为此时 static int count 是放在静态区,它是所有对象共享的,不会为每个对象单独生成一个count,可是当我们有三个不同的成员共同管理一块空间,而此时我们又用构造函数创建一个对象时候,count又会会变为1,所以这种方法还是不行 。
3)于是我们想到了引用计数,就是再创建对象的时候增加一个指针来存储当前有几个对象在管理_str 这块空间,代码实现如下:

class String
{
public:
    String(char *pstr = "")
    {
        _pstr = new char[strlen(pstr) + 1];
        strcpy(_pstr, pstr);
        _pcount = new int(1);
    }

    String(const String &s)
        :_pstr(s._pstr)
        , _pcount(s._pcount)
    {
        GetCount()++;
    }

    String &operator=(const String& s)
    {
    //1.this指向空间引用计数为1,这时候减减它以后还要释放这块空间
    //2.所指向的引用计数不为1,所以只用减减引用计数
        if (this != &s)
        {
            this->Release();
            _pstr = s._pstr;
            _pcount = s._pcount;
            GetCount()++;

        }
        return *this;
    }
//为当前对象重新开辟空间拷贝数据并设置它的引用计数,然后返回这个地址
    char& operator[](size_t index)
    {
        if (GetCount() > 1)
        {
            --GetCount();
            char* tmp = new char[strlen(_pstr) + 1];

            strcpy(tmp,_pstr);
            _pstr = tmp;
            _pcount = new int(1);
        }
        return _pstr[index];
    }


    ~String()
    {
        assert(_pstr);
        assert(_pcount);
        this->Release();
    }

    int& GetCount()
    {
        return *(this->_pcount);
    }

    void Printf()
    {
        cout << this->_pstr;
    }

    void Release()
    {
        --GetCount();
        if (GetCount() == 0)
        {
            delete[] _pstr;
            _pstr = NULL;
            delete _pcount;
            _pcount = NULL;
        }
    }

private:
    char* _pstr;
    int* _pcount;
};

还有一种方式是在对象的头部加上四个字节存一个int型的整数来标记当前空间有几个指针在管理,类似于new[ ]的实现,代码和上面的很类似:

class String
{
public:
    String(char* str = "")
    {
        if (str == NULL)
        {
            _str = new char[1 + 4];
            *(int*)(_str) = 1;
            _str += 4;
            *_str = '\0';
        }
        else
        {
            _str = new char[strlen(str) + 5];
            *(int*)_str = 1;
            _str += 4;
            strcpy(_str, str);
        }
    }
    String(const String& s)
        :_str(s._str)
    {
        ++GetRef();
    }
    String &operator=(const String & s)
    {
        if (_str != s._str)
        {
            --GetRef();
            ReduceRef();
        }
        else
        {
            _str = s._str;
        }
        return *this;
    }
    ~String()
    {
        --GetRef();
        ReduceRef();

    }
    //[]的重载 为了实现写时拷贝的数组[]下标形式的访问
    char &operator[](size_t index)
    {
        if (GetRef() > 1)
        {
            --GetRef();
            char* tmp = new char(strlen(_str) + 1 + 4);
            *(int*)tmp = 1;
            tmp += 4;
            strcpy(tmp, _str);
            _str = tmp;

        }
        return _str[index];
    }

public:
    int& GetRef()
    {
        return *(int*)(_str-4);
    }
    void ReduceRef()
    {
        if (0 == --GetRef())
        {
            _str += 4;
            delete[]_str;
            _str = NULL;
        }
    }

    void Printf()
    {
        cout << this->_str << endl ;
    }

private:
    char* _str;
};