• C++ 中的深拷贝与浅拷贝


      浅拷贝:又称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。举个简单的例子,你的小名叫西西,大名叫冬冬,当别人叫你西西或者冬冬的时候你都会答应,这两个名字虽然不相同,但是都指的是你。

      假设有一个String类,String s1;String s2(s1);在进行拷贝构造的时候将对象s1里的值全部拷贝到对象s2里。

      我们现在来简单的实现一下这个类

     1 #include <iostream>
     2 #include<cstring>
     3 
     4 using namespace std;
     5 
     6 class STRING 
     7 {
     8 public:
     9     STRING( const char* s = "" ) :_str( new char[strlen(s)+1] )
    10 
    11     {
    12         strcpy_s( _str, strlen(s)+1, s );
    13     }
    14     STRING( const STRING& s )
    15     {
    16         _str = s._str;
    17     }
    18     STRING& operator=(const STRING& s)
    19     {
    20         if (this != &s)
    21         {
    22             this->_str = s._str;
    23         }
    24         return *this;
    25     }
    26 
    27     ~STRING()
    28     {
    29         cout << "~STRING" << endl;
    30         if (_str)
    31         {
    32             delete[] _str;
    33             _str = NULL;
    34         }
    35     }
    36 
    37     void show()
    38     {
    39         cout << _str << endl;
    40     }
    41 private:
    42     char* _str;
    43 };
    44 
    45 int main()
    46 {
    47     STRING s1("hello linux");
    48     STRING s2(s1);
    49     s2.show();
    50 
    51     return 0;
    52 }

    其实这个程序是存在问题的,什么问题呢?我们想一下,创建s2的时候程序必然会去调用拷贝构造函数,这时候拷贝构造仅仅只是完成了值拷贝,导致两个指针指向了同一块内存区域。随着程序的运行结束,又去调用析构函数,先是s2去调用析构函数,释放了它指向的内存区域,接着s1又去调用析构函数,这时候析构函数企图释放一块已经被释放的内存区域,程序将会崩溃。s1和s2的关系就是这样的:

    进行调试程序发现s1和s2确实指向了同一块区域:

    所以程序会崩溃是应该的,那么这个问题应该怎么去解决呢?这就引出了深拷贝。

    深拷贝,拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。

    深拷贝实际上是这样的:

    深拷贝的拷贝构造函数和赋值运算符的重载传统实现:

     1 STRING( const STRING& s )
     2     {
     3         //_str = s._str;
     4         _str = new char[strlen(s._str) + 1];
     5         strcpy_s( _str, strlen(s._str) + 1, s._str );
     6     }
     7     STRING& operator=(const STRING& s)
     8     {
     9         if (this != &s)
    10         {
    11             //this->_str = s._str;
    12             delete[] _str;
    13             this->_str = new char[strlen(s._str) + 1];
    14             strcpy_s(this->_str, strlen(s._str) + 1, s._str);
    15         }
    16         return *this;
    17     }

    这里的拷贝构造函数我们很容易理解,先开辟出和源对象一样大的内存区域,然后将需要拷贝的数据复制到目标拷贝对象,

    那么这里的赋值运算符的重载是怎么样做的呢?

    这种方法解决了我们的指针悬挂问题,通过不断的开空间让不同的指针指向不同的内存,以防止同一块内存被释放两次的问题,还有一种深拷贝的现代写法:

     1 STRING( const STRING& s ):_str(NULL)
     2     {
     3         STRING tmp(s._str);// 调用了构造函数,完成了空间的开辟以及值的拷贝
     4         swap(this->_str, tmp._str); //交换tmp和目标拷贝对象所指向的内容
     5     }
     6 
     7     STRING& operator=(const STRING& s)
     8     {
     9         if ( this != &s )//不让自己给自己赋值
    10         {
    11             STRING tmp(s._str);//调用构造函数完成空间的开辟以及赋值工作
    12             swap(this->_str, tmp._str);//交换tmp和目标拷贝对象所指向的内容
    13         }
    14         return *this;
    15     }

    先来分析一下拷贝构造是怎么实现的:

    拷贝构造调用完成之后,会接着去调用析构函数来销毁局部对象tmp,按照这种思路,不难可以想到s2的值一定和拷贝构造里的tmp的值一样,指向同一块内存区域,通过调试可以看出来:

    在拷贝构造函数里的tmp:

    调用完拷贝构造后的s2:(此时tmp被析构)

    可以看到s2的地址值和拷贝构造里的tmp的地址值是一样

    关于赋值运算符的重载还可以这样来写:

    STRING& operator=(STRING s)
    {
      swap(_str, s._str);
      return *this;
    }

     1 #include <iostream>
     2 #include<cstring>
     3 
     4 using namespace std;
     5 
     6 class STRING 
     7 {
     8 public:
     9     STRING( const char* s = "" ) :_str( new char[strlen(s)+1] )
    10 
    11     {
    12         strcpy_s( _str, strlen(s)+1, s );
    13     }
    14     //STRING( const STRING& s )
    15     //{
    16     //    //_str = s._str; //浅拷贝的写法
    17     //    cout << "拷贝构造函数" << endl;
    18     //    _str = new char[strlen(s._str) + 1];
    19     //    strcpy_s( _str, strlen(s._str) + 1, s._str );
    20     //}
    21     //STRING& operator=(const STRING& s)
    22     //{
    23     //    cout << "运算符重载" << endl;
    24     //    if (this != &s)
    25     //    {
    26     //        //this->_str = s._str; //浅拷贝的写法
    27     //        delete[] _str;
    28     //        this->_str = new char[strlen(s._str) + 1];
    29     //        strcpy_s(this->_str, strlen(s._str) + 1, s._str);
    30     //    }
    31     //    return *this;
    32     //}
    33 
    34     STRING( const STRING& s ):_str(NULL)
    35     {
    36         STRING tmp(s._str);// 调用了构造函数,完成了空间的开辟以及值的拷贝
    37         swap(this->_str, tmp._str); //交换tmp和目标拷贝对象所指向的内容
    38     }
    39 
    40     STRING& operator=(const STRING& s)
    41     {
    42         if ( this != &s )//不让自己给自己赋值
    43         {
    44             STRING tmp(s._str);//调用构造函数完成空间的开辟以及赋值工作
    45             swap(this->_str, tmp._str);//交换tmp和目标拷贝对象所指向的内容
    46         }
    47         return *this;
    48     }
    49 
    50     ~STRING()
    51     {
    52         cout << "~STRING" << endl;
    53         if (_str)
    54         {
    55             delete[] _str;
    56             _str = NULL;
    57         }
    58     }
    59 
    60     void show()
    61     {
    62         cout << _str << endl;
    63     }
    64 private:
    65     char* _str;
    66 };
    67 
    68 int main()
    69 {
    70     //STRING s1("hello linux");
    71     //STRING s2(s1);
    72     //STRING s2 = s1;
    73     //s2.show();
    74     const char* str = "hello linux!";
    75     STRING  s1(str);
    76     STRING s2;
    77     s2 = s1;
    78     s1.show();
    79     s2.show();
    80 
    81     return 0;
    82 }

    参考与

    浅析C++中的深浅拷贝 - qq_39344902的博客 - CSDN博客
    https://blog.csdn.net/qq_39344902/article/details/79798297

  • 相关阅读:
    记账本微信小程序开发三
    记账本微信小程序开发二
    记账本微信小程序开发一
    一个Java系统测试
    河北省重大技术需求征集系统(5)
    河北省重大技术需求征集系统(4)
    河北省重大技术需求征集系统(3)
    河北省重大技术需求征集系统(2)
    《软件需求工程》阅读笔记02
    通过API操作HBase数据库
  • 原文地址:https://www.cnblogs.com/cxq0017/p/10617313.html
Copyright © 2020-2023  润新知