string类的深拷贝和浅拷贝

1.string的浅拷贝

如果一个string只显示的给出构造函数和析构函数,拷贝构造函数和赋值运算符重载使用系统默认的,当进行拷贝和赋值时,会出现什么结果:

#include <string.h>
class String
{
public:
    String(const char* str = "");
    ~String();
private:
    char* _str;

};

//构造函数
String::String(const char* str)
    :_str( new char[strlen(str) + 1])
{
    if(NULL != str)
        strcpy(_str, str);
}

//析构函数
String::~String()
{
    delete[] _str;
}

int main()
{
    String str1("abcde");
    String str2(str1);
    String str3;
    str3 = str1;
    return 0;
}

首先试着运行一下,发现程序会崩溃,接下来进行断点调试:

1.运行完String str1("abcde"),结果如下:
这里写图片描述
此时可以看到,str1运行正常。

2.运行完String str2(str1),结果如下:
这里写图片描述
可以看到,str2也运行正常,但是值得注意的一一点是,str2str1 指向的是同一块空间。

3.运行完str3 = str1,结果如下:
这里写图片描述
str3也正常生成,但是同样的,str3和str1指向同样的地址空间。

4.在运行return 0时,由于会调用三个对象的析构函数来释放空间,但是由于三个对象所指的地址空间是一样的,因此先析构str3(后生成的先释放),释放空间,但是在释放str2时由于他的空间已经被释放过了,因此在这里就会报错。

还有一个要注意的地方是,str3 = str1时由于是浅拷贝,因此对象str3_str的值会被str1_str的值替代,造成内存泄漏。


2.关于浅拷贝的危害

在说浅拷贝的危害之前,我先来说一下什么是浅拷贝:

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来,如果对象中管理资源, 最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规

  1. 因此当类里面有指针对象时,拷贝构造和赋值运算符重载只进行值拷贝,两个对象共用同一块空间,对象销毁时程序会发生内存访问违规

  2. 在赋值运算符重载是由于是传值,导致指针的指向有所改变,造成内存泄漏问题。

  3. 如果使用拷贝构造和赋值运算符重载,当修改一个对象的值是,由于指向的同一空间,因此会导致其他对象的值一起改变,因此对象的存在将毫无意义。

这里我们要杜绝浅拷贝,那么解决方法就是使用深拷贝。


3. 深拷贝解决方法

1.解决浅拷贝方式一:普通版深拷贝

//拷贝构造函数--普通版
String::String(const String& s)
    :_str(new char[strlen(s._str)+1])
{
    if (s._str != NULL)
        strcpy(_str, s._str);
}

//赋值运算符重载--普通版
String& String::operator=(const String& s)
{
    if (NULL != s._str)
    {
        delete[] _str;
        _str = new char[strlen(s._str) + 1];
        strcpy(_str, s._str);
    }

    return *this;
}

2.解决浅拷贝方式二:简洁版的深拷贝

void String::swap(char** a1, char** a2)
{
    char *temp = *a1;
    *a1 = *a2;
    *a2 = temp;
}

//拷贝构造函数
String::String(const String& s)
{
    String temp(s._str);
    swap(&temp._str, &_str);
}

//赋值运算符重载--普通版
String& String::operator=(const String& s)
{
    String temp(s._str);
    swap(&temp._str, &_str);
    return *this;
}

拷贝构造函数(简洁版)的调运过程图示,如下:
这里写图片描述

赋值函数(简洁版)的调运过程图示,如下:
这里写图片描述

3.解决浅拷贝方式三:引用计数实现(仍是浅拷贝)

#include<string.h>
#pragma warning (disable:4996)
class String
{
public:
    String(const char* str = "");
    String(const String& s);
    String& operator=(const String& s);
    ~String();
private:
    char* _str;
    int *_pCount; 
};

//构造函数
String::String(const char* str)
    :_str(new char[strlen(str) + 1])
    ,_pCount(new int(1))
{
    if (NULL != str)
        strcpy(_str, str);
}

//拷贝构造函数
String::String(const String& s)
{
    _str = s._str;
    _pCount = s._pCount;
    (*_pCount)++;
}

//赋值运算符重载
String& String::operator=(const String& s)
{
    delete[] _str;
    _str = s._str;
    _pCount = s._pCount;
    (*_pCount)++;
    return *this;
}

//析构函数
String::~String()
{
    if (--(*_pCount) == 0)
    {
        delete[] _str;
        delete _pCount;
    }
}

int main()
{
    String str1("abcde");
    String str2(str1);
    String str3("1234");
    str3 = str1;
    return 0;
}

在这里要说明的是:此引用计数选用的是指针,并且在调用构造函数时,分配空间并初始化。

在选用指针之前,我尝试使用成员变量和静态成员变量来做引用计数,结果发现,成员变量和

静态成员变量都不可以用来做引用计数,理由如下:

  • 成员变量做引用计数

    在调用拷贝构造函数或者运算符重载时,后面的对象对引用计数进行修改,仅仅只会印象到自己的这一数据,并不会改变其他对象的数据。因为成员函数是属于单个对象的因此不能用来控制全局。

  • 静态成员变量做引用计数

    虽然,静态成员变量是属于类的,可以达到控制全局的作用,但是唯一的一点不好就是,一个类只有一份该变量,造成的结果是,在已有了一个对象,且该对象有许多拷贝指向该对象,即引用计数大于1,这时如果重新定义一个新的对象,那么就会在构造函数里面对引用变量进行初始化(使引用变量等于1),问题就是该引用变量只有一份,且是属于类的,那么之前引用对象的记录,也就同时被刷新了,数据也乱掉了,因此不能使用静态成员变量来做引用计数。

  • 使用指针做引用变量的好处有:

    使用指针做引用变量不仅能够达到要求,还会为每一为新的不同对象创建一份引用计数,不会影响其他相同对象的引用计数。


4.写时拷贝(cow:copy on write)

在上述方法三中使用引用计数解决浅拷贝的方式存在问题:如果多个String类型的对象共用同一块内存空间,改变其中一个String类对象的字符,其几个同时被修改,不科学 。

这时我们就需用了解一下写时拷贝,即当多个String类对象共用同一块空间时,如果一个有可能改变一个对象中的字符内容,就将该对象分离出来,不要和别的对象共享空间,例如operator[]。

char& String::operator[](int num)
{
    String temp(_str);
    swap(&_str, &temp._str);
    swap(&_pCount, &temp._pCount);
    return *((this->_str)+num);
}

int main()
{
    String str1("abcde");
    String str2(str1);
    String str3("1234");
    str3 = str1;

    str3[0] = 'A';
    return 0;
}

构建一个单独的临时对象temp,并且将temp和要操作的对象进行内容指针交换,即交换_str指向和_pCount的指向, 即用temp完全替代要操作对象的原位置,在函数返回结束时,会释放临时对象,即释放要操作对象原来的空间,且保持引用计数减一,达到引用计数的作用。


5.总结

在这里基本上将string类的浅拷贝和深拷贝介绍完了,因为浅拷贝的出现场景不止在string类里面,因此希望大家多多注意浅拷贝的出现,并且学会深拷贝的解决方法,我的建议是主要掌握前两种方法即可,对于第三种,算是一个优化吧,在性能上肯定比前两种要好的多,但是同样的不好理解,希望大家能够多思考加以理解。最后希望大家能指出我的错误和不足的地方,我会加以改正的。

猜你喜欢

转载自blog.csdn.net/xiaozuo666/article/details/80203259