C++ 浅拷贝、深拷贝(基于String类)

C++ string


  • 我们在C语言中定义字符串是通过char *来定义的,C++标准库为我们提供了一个可以替代C语言定义字符串的方式—–string,通过引用#include头文件,我们就可以很方便的操作字符串了
  • 下面演示一下string的使用:
    这里写图片描述
  • 我们可以看到,通过string操作字符串和我们操作内置类型数据一样方便
  • 我们已经学习过类和对象基本知识,下面我们来模拟实现一下C++标准库中的string类String,首先实现一下构造、拷贝构造以及析构函数,因为String是我们的自定义类型,所以我们需要自己来实现相关操作
  • 实现代码如下:
class String
{
public:
    String(const char* s="")
        :_str(new char[strlen(s)+1])
        ,_sz(strlen(s))
        ,_capacity(strlen(s)+1)
    {
        strcpy(_str, s);
    }

    String(const String& s)
        :_str(s._str)
        , _sz(s._sz)
        , _capacity(s._capacity)
    {}

    ~String()
    {
        if (_str)
            delete[] _str;
        _str = NULL;
        _sz = 0;
        _capacity = 0;
    }
private:
    char* _str;
    int _sz;
    int _capacity;
};
  • 我们简单的测试一下我们的String类:
void TestString()
{
    String s1("string");
    String s2(s1);

}
  • 我们通过监视窗口看一下我们的对象是否构造成功:
    这里写图片描述
  • s1,s2构造成功,它们指向同一块空间
  • 程序运行结果如下:
    这里写图片描述
  • 我们观察到程序在编译的时候是没有问题的,但是运行时崩溃,这是什么原因呢?
  • 结合我们刚才实现的三个函数我们分析:我们创建了一个String对象s1,为其开辟空间并对其赋值为“string”;然后我们利用s1对象拷贝构造出s2对象,我们的拷贝构造函数让s2和s1指向同一块空间;析构函数先析构s2对象,再析构s1对象。
  • 问题来了:我们的s1,s2对象指向的空间是同一块,s2调用析构函数释放了空间,因为s1._str一直指向申请空间,所以不为空,调用析构函数释放已经被s2释放的空间,这就是是导致程序崩溃的原因,释放一块不存在的空间是非法的

浅拷贝


  • 上面我们已经实现了String类的基本函数,也分析出了导致程序崩溃的原因,其实这个原因我们有一个专门的名词来称呼它——浅拷贝
  • 浅拷贝: 也称位拷贝 ,编译器只是直接将指针的值拷贝过来,结果多个对象共用 同 一块内存,当一个对象将这块内 存释放掉之后, 另 一些对象不知道该块空间已经还给了系统,以为还有效, 所以在对这段内存进行操作的时候,发生了违规访问
  • 按照我们的传统写法,赋值运算符重载应该也会出现上面的问题:
String& operator=(const String& s)
    {
        if (this != &s)
            _str = s._str;
        return *this;
    }
  • 通过监视窗口我们可以看到,s1,s2,s3依然是指同一块内存空间,所以它们的析构也会出现问题
    这里写图片描述
  • 所以我们总结一下浅拷贝存在的问题:1)同一空间释放多次,程序崩溃;2)内存泄漏

那么我们应该如何和解决浅拷贝带来的问题呢?

  • 类中存在指针对象,既然简单的值拷贝不能让两个对象拥有自己独立的空间,那么我们是否可以通过拷贝一份同样大小的空间然后在将值进行拷贝呢?这就是我们的深拷贝问题

深拷贝


  • 深拷贝:拷贝字符串并将副本的地址赋给str成员,而不仅仅是拷贝字符串地址。这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。则调用析构函数都将释放不同的字符串而不是去释放已经释放的字符串
  • 下面是我们通过深拷贝的方式实现的拷贝构造函数:
    String(const String& s)
        :_str(new char[strlen(s._str)+1])
    {
        if (this != &s)
        {
            strcpy(_str, s._str);
            _sz = s._sz;
            _capacity = s._capacity;
        }
    }

    String& operator=(const String& s)
    {
        if (this != &s)
        {
            char *tmp = new char[strlen(s._str) + 1];
            strcpy(tmp, s._str);
            _sz = s._sz;
            _capacity = s._capacity;
        }
    }
  • 我们通过监视窗口观察一下结果:
    这里写图片描述
  • 我们可以看到,s1,s2,s3三个对象各自都有各自的地址,程序通过运行,也没有问题。因为三个对象指向的地址不同,所以它们调用析构函数释放的内存也就不会时同一块了,可以看到深拷贝很好的解决了浅拷贝带来的问题

关于深拷贝,我们还有以下更简洁的实现方式(现代写法):

  • 我们可以考虑:我们构造函数是需要开空间的,既然我们已经实现了构造函数,为什么不能利用构造函数呢?所以我们可以通过构造一个新的对象,并为它赋初值,然后交换this指针与这个新对象的指向,来达到我们拷贝构造和赋值运算符重载,因为tmp是个临时对象,出了作用域它就会被销毁,所以并没有额外的开销
  • 代码实现:

    void Swap(String& s)
    {
        swap(_str, s._str);
        swap(_sz, s._sz);
        swap(_capacity, s._capacity);
    }

    String(const String& s)
        :_str(NULL)
        , _sz(0)
        , _capacity(0)
    {
        //注意:这里调用的是构造函数
        String tmp = s._str;
        this->Swap(tmp);
    }

    String& operator=(const String& s)
    {
        String tmp = s._str;
        this->Swap(tmp);
        return *this;
    }
  • 程序运行,结果正确
  • 关于我们的赋值运算符重载,我们还有一种更简洁的写法:
    String& operator=(String& s)
    {
        s.Swap(*this);
        return *this;
    }
  • 通过监视窗口观察结果如下:
    这里写图片描述
  • 我们发现虽然将s1的值赋给了s3,但是s1的值却为空,这是因为s3调用了缺省的构造函数,被初始化为空,s1和s3进行交换,s3得到了s1的值,达到了赋值的目的,s1得到了s3的空值。这种方法减少了构造函数的开销,比上面更高效,但是会改变原值,实际应用中我们可以根据需要选择实现方法

猜你喜欢

转载自blog.csdn.net/aurora_pole/article/details/80234809