【C++】 深浅拷贝浅析

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

我们知道,对于一般对象:

    int a = 1;
    int b = 2;

这样的赋值,复制很简单,但对于类对象来说并不一般,因为其内部包含各种类型的成员变量,在拷贝过程中就会出现问题

例如:

#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;
}

运行结果:

我们发现,编译通过了,但是崩溃了 =  =ll ,这就是浅拷贝带来的问题。

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

String(const String& s)
     {}

但是,编译器提供的缺省函数并不是十全十美的。

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


用图形象化为:

在进行对象复制后,事实上s1、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;
}
时间: 2024-10-19 00:35:08

【C++】 深浅拷贝浅析的相关文章

python学习笔记4:基础(集合,collection系列,深浅拷贝)

转载至:http://www.cnblogs.com/liu-yao/p/5146505.html 一.集合 1.集合(set): 把不同的元素组成一起形成集合,是python基本的数据类型.集合元素(set elements):组成集合的成员 python的set和其他语言类似, 是一个无序不重复元素集, 基本功能包括关系测试和消除重复元素. 集合对象还支持union(联合), intersection(交), difference(差)和sysmmetric difference(对称差集)

Python3.5(十)深浅拷贝问题

[可变对象-不可变对象] 在Python中不可变对象指:一旦创建就不可修改的对象,包括字符串,元祖,数字 在Python中可变对象是指:可以修改的对象,包括:列表.字典 >>> L1 = [2,3,4] #L1变量指向的是一个可变对象:列表 >>> L2 = L1 #将L1值赋给L2后,两者共享引用同一个列表对象[1,2,3,4] >>> L1[0] = 200 #因为列表可变,改变L1中第一个元素的值 >>> L1; L2 #改变后

C++模板实现动态顺序表(更深层次的深浅拷贝)与基于顺序表的简单栈的实现

前面介绍的模板有关知识大部分都是用顺序表来举例的,现在我们就专门用模板来实现顺序表,其中的很多操作都和之前没有多大区别,只是有几个比较重要的知识点需要做专门的详解. 1 #pragma once 2 #include<iostream> 3 #include<string> 4 #include<stdlib.h> 5 using namespace std; 6 7 template <class T> 8 class Vector 9 { 10 publ

Python深浅拷贝

深浅拷贝 深浅拷贝分为两部分,一部分是数字和字符串另一部分是列表.元组.字典等其他数据类型. 数字和字符串 对于数字和字符串而言,赋值.浅拷贝和深拷贝无意义,因为他们的值永远都会指向同一个内存地址. # 导入copy模块>>> import copy# 定义一个变量var1>>> var1 = 123# 输出var1的内存地址>>> id(var1)1347747440>>> var2 = var1# var2的内存地址和var1相同

Python 从零学起(纯基础) 笔记 之 深浅拷贝

深浅拷贝 1. import  copy#浅拷贝copy.copy()#深拷贝copy.deepcopy()#赋值 = 2.   对于数字和字符串而言,赋值.浅拷贝和深拷贝无意义,因为其永远指向同一个内存地址. 对于 字典.元组.列表 而言,进行赋值.浅拷贝和深拷贝时,其内存地址的变化是不同的. 浅拷贝,在内存中只额外创建第一层数据. 深拷贝,在内存中将所有的数据重新创建一份(排除最后一层,即:Python内部对字符串和数字的优化)   1 import copy 2 n1 = {"k1&quo

深浅拷贝的使用场景分析

浅 复 制:在复制操作时,对于被复制的对象的每一层复制都是指针复制. 深 复 制:在复制操作时,对于被复制的对象至少有一层复制是对象复制. 完全复制:在复制操作时,对于被复制的对象的每一层复制都是对象复制. 注:1.在复制操作时,对于对象有n层是对象复制,我们可称作n级深复制,此处n应大于等于1. 2.对于完全复制如何实现(目前通用的办法是:迭代法和归档),这里后续是否添加视情况而定, 暂时不做讲解.  3.指针复制俗称指针拷贝,对象复制也俗称内容拷贝. 4.一般来讲, 浅层复制:复制引用对象的

Python基础:深浅拷贝

对于数字.字符串深浅拷贝: import copy num = 0 copy_num = copy.copy(num) print("These are normal copy").center(60,'*') print(num,id(num)) print(copy_num,id(copy_num)) print("These are deep copy").center(60,'*') deep_copy_num = copy.deepcopy(num) pr

3.python基础补充(集合,collection系列,深浅拷贝)

一.集合 1.集合(set): 把不同的元素组成一起形成集合,是python基本的数据类型.集合元素(set elements):组成集合的成员 python的set和其他语言类似, 是一个无序不重复元素集, 基本功能包括关系测试和消除重复元素. 集合对象还支持union(联合), intersection(交), difference(差)和sysmmetric difference(对称差集)等数学运算. sets 支持 x in set, len(set),和 for x in set.作

【python之路13】python的深浅拷贝

深浅拷贝 一.数字和字符串 对于 数字 和 字符串 而言,赋值.浅拷贝和深拷贝无意义,因为其永远指向同一个内存地址. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import copy # ######### 数字.字符串 ######### n1 = 123 # n1 = "i am alex age 10" print(id(n1)) # ## 赋值 ## n2 = n1 print(id(n2)) # ## 浅拷贝 ## n2 = copy.copy(