c++

c++深拷贝与浅拷贝

For study!

Posted by Winray on February 21, 2016
  • 术语
    • 浅拷贝 shallow copy
    • 深复制 deep copy
默认拷贝构造函数
  • 很多时候在我们不定义拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值。
  • 然而,拷贝构造函数没有处理静态数据成员。
浅拷贝
  • 所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码:
#include<iostream>
#include<assert.h>
using namespace std;

class Rect
{
public:
    Rect()
    {
     p=new int(100);
    }
   
    ~Rect()
    {
     assert(p!=NULL);
        delete p;
    }

private:
    int width;
    int height;
    int *p;
};


int main()
{
    Rect rect1;
    Rect rect2(rect1);
    return 0;
}
  • 在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。
深拷贝
  • 在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:
#include<iostream>
#include<assert.h>
using namespace std;

class Rect
{
public:
    Rect()
    {
     p=new int(100);
    }
    
    Rect(const Rect& r)
    {
     width=r.width;
        height=r.height;
     p=new int(100);
        *p=*(r.p);
    }
     
    ~Rect()
    {
     assert(p!=NULL);
        delete p;
    }

private:
    int width;
    int height;
    int *p;
};

int main()
{
    Rect rect1;
    Rect rect2(rect1);
    return 0;
}

Tips

防止默认拷贝发生(禁止复制 noncopyable)
  • 通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。
class CExample { //防止按值传递
private: 
    int a; 
public: 
    CExample(int b) { //构造函数
        a = b;
        cout << "creat: " << a << endl; 
    }

private: 
    CExample(const CExample& C); //拷贝构造函数,只是声明
public: 
    ~CExample() { 
        cout<< "delete: " << a << endl; 
    } 
  
    void Show () 
    { 
        cout << a << endl; 
    } 
}; 
  
void g_Fun(CExample C) 
{ 
    cout << "test" << endl; 
} 
  
int main() 
{ 
    CExample test(1); 
    //g_Fun(test);   //按值传递将出错
      
    return 0; 
}
  • 为什么拷贝构造函数必须是引用传递,不能是值传递?
    • 简单的回答是为了防止递归引用。
    • 具体一些可以这么讲:当一个对象需要以值方式传递时,编译器会生成代码调用它的拷贝构造函数以生成一个复本。如果类A的拷贝构造函数是以值方式传递一个类A对象作为参数的话,当 需要调用类A的拷贝构造函数时,需要以值方式传进一个A的对象作为实参; 而以值方式传递需要调用类A的拷贝构造函数;结果就是调用类A的拷贝构造函数导 致又一次调用类A的拷贝构造函数,这就是一个无限递归。
  • 拷贝构造函数的作用。
    • 作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。
  • 析构函数为什么一般情况下要声明为虚函数?
    • 虚函数是实现多态的基础,当我们通过基类的指针是析构子类对象时候,如果不定义成虚函数,那只调用基类的析构函数,子类的析构函数将不会被调用。如果定义为虚函数,则子类父类的析构函数都会被调用。
  • 什么情况下必须定义拷贝构造函数?
    • 当类的对象用于函数值传递时(值参数,返回类对象),拷贝构造函数会被调用。如果对象复制并非简单的值拷贝,那就必须定义拷贝构造函数。例如大的堆栈数据拷贝。如果定义了拷贝构造函数,那也必须重载赋值操作符。
  • 构造函数能否重载,析构函数能否重载,为什么?
    • 函数重载就是同一函数名的不同实现,并且能在编译时能与一具体形式匹配,这样参数列表必须不一样。由于重载函数与普通函数的差别是没有返回值,而返回值不能确定函数重载,所以构造函数可以重载; 析构函数的特点是参数列表为空,并且无返回值,从而不能重载。

总结

  • 当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
  • 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
  • 类中可以存在超过一个拷贝构造函数。