【C++】 浅析深浅拷贝

  C++中深拷贝和浅拷贝的问题是很值得咱们注意的知识点,若是编程中不注意,可能会出现疏忽,致使bug。本文就详细讲讲C++深浅拷贝的种种。ios

  咱们知道,对于通常对象:编程

    int a = 1;
    int b = 2;

  这样的赋值,复制很简单,但对于类对象来讲并不通常,由于其内部包含各类类型的成员变量,在拷贝过程当中就会出现问题ide

例如:函数

#include <iostream>
using namespace std;

class String
{
public:

    String(char str = "")
        :_str(new char[strlen(str) + 1]) // +1是为了不空字符串致使出错
    {
        strcpy(_str , str);
    }
    
    // 浅拷贝
    String(const String& s)
        :_str(s._str)
    {}
    
    ~String()
    {
        if(_str)
        {
            delete[] _str;
            _str = NULL;
        }
        cout<<"~String()"<<endl;
    }
    
    void Display()
    {
        cout<<_str<<endl;
    }
    
private:
    char *_str;
};

void Test()
{
    String s1("hello");
    String s2(s1);
    String.Display();
    
}

int main()
{
    Test();
    
    return 0;
}

运行结果:学习

wKiom1bqRJ7CC1j7AABxvgeu1pI402.png

咱们发现,编译经过了,可是崩溃了 =  =ll ,这就是浅拷贝带来的问题。this


   事实是,在对象拷贝过程当中,若是没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。原型以下:spa

String(const String& s)
     {}

可是,编译器提供的缺省函数并非十全十美的。设计


      缺省拷贝构造函数在拷贝过程当中是按字节复制的,对于指针型成员变量只复制指针自己,而不复制指针所指向的目标--浅拷贝。指针


用图形象化为:对象


wKioL1bqSOax7GQiAAAb-81vW_4778.png

  在进行对象复制后,事实上s一、s2里的成员指针 _str 都指向了一块内存空间(即内存空间共享了),在s1析构时,delete了成员指针 _str 所指向的内存空间,而s2析构时一样指向(此时已变成野指针)而且要释放这片已经被s1析构函数释放的内存空间,这就让一样一片内存空间出现了 “double free” ,从而出错。而浅拷贝还存在着一个问题,由于一片空间被两个不一样的子对象共享了,只要其中的一个子对象改变了其中的值,那另外一个对象的值也跟着改变。因此这不是咱们想要的结果,同事也不是真正意义上的复制。


为了解决浅拷贝问题,咱们引出深拷贝,即自定义拷贝构造函数,以下:

String(const String& s)
        :_str(new char[strlen(s._str) + 1])
    {
        strcpy(_str , s._str);
    }

这样在运行就没问题了。


那么,程序中还有没有其余地方用到拷贝构造函数呢?

答案:当函数存在对象型的参数(即拷贝构造)或对象型的返回值(赋值时的返回值)时都会用到拷贝构造函数。


而拷贝赋值的状况基本上与拷贝复制是同样的。只是拷贝赋值是属于操做符重载问题。例如在主函数如有:String s3; s3 = s2; 这样系统在执行时会调用系统提供的缺省的拷贝赋值函数,原型以下:


void operator = (const String& s) 
    {}

咱们自定义的赋值函数以下:

void operator=(const String& s)
   {
      if(_str != s._str)
      {
        strcpy(_str,s._str);
      }
      return *this;
   }


  可是这只是新手级别的写法,考虑的问题太少。咱们知道对于普通变量来说a=b返回的是左值a的引用,因此它能够做为左值继续接收其余值(a=b)=30,这样来说咱们操做符重载后返回的应该是类对象的引用(不然返回值将不能做为左值来进行运算),以下:

String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            strcpy(_str,s._str);
        }
        return *this;
    }

 

  而上面这种写法其实也有问题,由于在执行语句时,_str 已经被构造已经分 配了内存空间,可是如此进行指针赋值,_str 直接转而指向另外一片新new出来的内存空间,而丢弃了原来的内存,这样便形成了内存泄露。应更改成:

String& operator=(const String& s)
    {
        if(_str != s._str)  
        {
            delete[] _str;
            _str = new char[strlen(s._str) + 1];
            strcpy(_str,s._str);
        }
        return *this;
    }

同时,也考虑到了本身给本身赋值的状况。


  但是这样写就完善了吗?是否要再仔细思索一下,还存在问题吗?!其实我能够告诉你,这样的写法也顶多算个初级工程师的写法。前面说过,为了保证内存不泄露,咱们前面 delete[]  _str,而后咱们在把new出来的空间给了_str,可是这样的问题是,你有考虑过万一 new 失败了呢?!内存分配失败,m_psz没有指向新的内存空间,可是它却已经把旧的空间给扔掉了,因此显然这样的写法依旧存在着问题。通常高级工程师的写法会是这样的:

String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            char *tmp = new char[strlen(s._str) + 1];
            strcpy(tmp , s._str);
            delete[] _str;
            _str = tmp;
        }
        return *this;
    }

这样写就比较全面了。


可是!!!还有元老级别的大师写的更加简便的拷贝构造和赋值函数,咱们一睹为快:

<元老级拷贝构造函数>

    String(const String& s)
        :_str(NULL)
    {
        String tmp = s._str;
        swap(_str , tmp._str);
    }

<元老级赋值函数>

// 1.
   String& operator=(const String& s)
   {
     if(_str != s._str)
     {
       String tmp = s._str;
       swap(_str , tmp._str);
     }
     return *this;
   }
// 2.
    String& operator=(String& s)//在此会拷贝构造一个临时的对象s
   {
     if(_str != s._str)
     {
       swap(_str ,s._str);// 交换this->_str和临时生成的对象数据成员s._str,离开做用域会自动析构释放
     }
     return *this;
   }

看出端倪了么?


  事实上,这是借助了以上自定义的拷贝构造函数。定义了局部对象 tmp,在拷贝构造中已经为 tmp 的成员指针分配了一块内存,因此只须要将 tmp._str 与this->_str交换指针便可,简化了程序的设计,由于 tmp 是局部对象,离开做用域会调用析构函数释放交换给 tmp._str 的内存,避免了内存泄露。

这是很是值得咱们学习和借鉴的。


这是本人对C++深浅拷贝的理解,如有纰漏,欢迎留言指正 ^_^


附注总体代码:

#include <iostream>
using namespace std;

class String
{
public:

    String(char *str = "")
        :_str(new char[strlen(str) + 1])
    {
        strcpy(_str , str);
    }
    
    // 浅拷贝
    String(const String& s)
        :_str(s._str)
    {}


    //赋值运算符重载
    //有问题,会形成内存泄露。。。
    String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            strcpy(_str,s._str);
        }
        return *this;
    }

    // 深拷贝  <传统写法>
    String(const String& s)
        :_str(new char[strlen(s._str) + 1])
    {
        strcpy(_str , s._str);
    }
    
    //赋值运算符重载
    //一.  这种写法有问题,万一new失败了。。
    String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            delete[] _str;
            _str = new char[strlen(s._str) + 1];
            strcpy(_str,s._str);
        }
        return *this;
    }

    //二.  对上面的方法改进,先new后delete,若是new失败也不会影响到_str原来的内容
    String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            char *tmp = new char[strlen(s._str) + 1];
            strcpy(tmp , s._str);
            delete[] _str;
            _str = tmp;
        }
    }

    // 深拷贝  <现代写法>
    String(const String& s)
        :_str(NULL)
    {
        String tmp = s._str;
        swap(_str , tmp._str);
    }
    
    //赋值运算符的现代写法一:
    String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            String tmp = s._str;
            swap(_str , tmp._str);
        }
        return *this;
    }
    //赋值运算符的现代写法二:
    String& operator=(String& s) //在此会拷贝构造一个临时的对象s
    {
        if(_str != s._str)
        {
            swap(_str ,s._str);//交换this->_str和临时生成的对象数据成员s._str,离开做用域会自动析构释放
        }
        return *this;
    }

    ~String()
    {
        if(_str)
        {
            delete[] _str;
            _str = NULL;
        }
        cout<<"~String()"<<endl;
    }

    void Display()
    {
        cout<<_str<<endl;
    }

private:
    char *_str;
};

void Test()
{
    String s1;
    String s2("hello");
    String s3(s2);
    String s4 = s3;

    s1.Display();
    s2.Display();
    s3.Display();
    s4.Display();

}

int main()
{
    Test();

    return 0;
}
相关文章
相关标签/搜索