深入探索C++对象模型(八) 拷贝构造函数 (浅拷贝问题)

对于默认的拷贝赋值操作符,在如下情况下不会表现出按位拷贝(bitwise copy:关于按位拷贝,实际就是不使用拷贝构造函数或者拷贝赋值操作符,这里的不使用是指编译器根本不会产生,而是采用按位拷贝对象数据的方式,若对象中含有指针,此时的指针只是地址级别的浅拷贝,可能会引起内存问题)

(位拷贝就是浅拷贝,值拷贝就是深拷贝)

    a. 当类内带有一个含有拷贝赋值操作符的成员变量时。

    b. 当类的基类含有拷贝赋值操作符时。

    c. 当类声明了虚函数时,此时不能直接拷贝右端类对象的虚函数指针,因为右边可能是子类对象。

    d. 当类继承自虚基类时(不管此虚基类有无拷贝赋值操作符)。

这时候浅拷贝就会出现问题

例如:

[cpp]  view plain  copy
  1. class Point  
  2. {  
  3. public:  
  4.     Point(float x = 0.0, float y = 0.0);  
  5.     //没有虚函数  
  6. protected:  
  7.     float _x, _y;  
  8. };  
含有拷贝赋值操作符:
[cpp]  view plain  copy
  1. inline Point& Point::operator=(const Point& p)  
  2. {  
  3.     _x = p._x;  
  4.     _y = p._y;  
  5.     return *this;  
  6. }  
此时,有一个Point3d类虚拟继承自Point类:
[cpp]  view plain  copy
  1. class Point3d : virtual public Point {  
  2. public:  
  3.     Point3d(float x = 0.0, float y = 0.0, float z = 0.0)  
  4.     //...  
  5. protected:  
  6.     float _z;  
  7. };  
由于上述b和d原因的存在,此时编译器会为Point3d合成一个拷贝赋值操作符(由于Point3d未定义此操作符),看起来像这样:
[cpp]  view plain  copy
  1. inline Point3d& Point3d::operator=(Point3d* const thisconst Point3d& p)  
  2. {  
  3.    //invoke base's operator=  
  4.     this->Point::operator=(p);//or can invoke like this: (*(Point*)this) = p;  
  5.     //memberwise copy the derived class members  
  6.     _z = p.z;  
  7.     return *this;  
  8. }  
注意,对于拷贝赋值操作符,编译器没有办法压制对于基类的多次拷贝赋值(可以结合上一篇中关于编译器如何压制子类构造函数中多次构造基类的问题),例如,类Vertex也虚拟继承自Point(//class Vertex: virtual public Point { //... };),Vertex3d派生自Point3d和Vertex,Vertex3d的拷贝赋值运算符看起来是这样的,此时由于Point3d和Vertex的operator=中也会调用Point::operator=,所以此时Point::operator=在类Vertex3d中调用了三次:
[cpp]  view plain  copy
  1. inline Vertex3d& Vertex3d::operator=(const Vertex3d& v)  
  2. {  
  3.     this->Point::operator=(v);  
  4.     this->Point3d::operator=(v);//会调用:this->Point::operator=(v);  
  5.     this->Vertex::operator=(v);//也会调用:this->Point::operator=(v);  
  6. }  

之所以会出现重复调用是因为获取拷贝赋值操作符的地址是合法的(关于成员函数指针可以参考中,”指向成员函数的指针部分

[cpp]  view plain  copy
  1. typedef Point3d& (Point3d::* pmfPoint3d)(const Point3d&);  
  2. pmfPoint3d pmf = &Point3d::operator=;  
  3. (p.*pmf)(x);  

关于此点,C++标准中的说法是:“我们并没有规定那些代表虚基类的子对象是否应该被隐喻定义的(implicitly defined)拷贝赋值操作符指派(assign)内容一次以上。”

由此可见,拷贝赋值操作符在虚拟继承的情况下行为不佳。

因此一条建议就是:不要在任何虚基类中声明数据成员


浅拷贝构造函数 
看一段拷贝构造函数的代码

#include <iostream>
#include <cstring>
using namespace std;
class Array{
public :
    Array(){
        cout<<"Array()"<<endl;
    }
    Array(const Array &arr){ /// 拷贝构造函数
        m_iCount = arr.m_iCount;
        cout<<"Array &"<<endl;
    }

    virtual ~Array(){
        cout<<"~Array()"<<endl;
    }
    void setCount(int _count){
        m_iCount = _count;
    }
    int getCount(){
        return m_iCount;
    }
private :
    int m_iCount;
};
int main(){
    Array arr1;
    arr1.setCount(5);
    Array arr2(arr1);///浅拷贝
    cout<<"arr2 m_iCount "<<arr2.getCount()<<endl;
    return 0;
}

类Array实例化一个arr1的对象,并给数据成员m_iCount赋值为5。 
接着类Array实例化一个对象arr2并将arr1的数据成员的值拷贝给arr2 
运行代码,显然arr1和arr2的数据成员m_iCount的值都为5,系统给arr1,arr2分配了内存空间并使得arr1的值复制给了arr2 
接下来再看一段代码:

#include <iostream>
#include <cstring>
using namespace std;
class Array{
public :
    Array(int _count){
        m_iCount = _count;
        m_pArr = new int[m_iCount];
        cout<<"Array()"<<endl;
    }
    Array(const Array &arr){ /// 拷贝构造函数
        m_iCount = arr.m_iCount;
        m_pArr = arr.m_pArr; ///两个指针指向同一块内存
        cout<<"Array &"<<endl;
    }

    virtual ~Array(){
        delete []m_pArr;
        m_pArr = NULL;
        cout<<"~Array()"<<endl;
    }
    void setCount(int _count){
        m_iCount = _count;
    }
    int getCount(){
        return m_iCount;
    }
    void printAddr(){
        cout<<"m_pArr : "<<m_pArr<<endl;
    }
private :
    int m_iCount;
    int *m_pArr;
};
int main(){
    Array arr1(5);
    Array arr2(arr1);
    arr1.printAddr();
    arr2.printAddr();
    return 0;
}

我们在类中添加一个数据成员int型的指针m_pArr,实例化一个对象arr1并给数据成员m_iCount赋值为5,与此同时系统也需要给另一个数据成员m_pArr在堆区分配内存空间 
接着实例化一个对象arr2并将arr1的值复制给arr2,系统调用拷贝构造函数。将arr1.m_iCount赋值给arr2.m_iCount,将arr1.m_pArr赋值给arr2.m_iArr。因为在拷贝构造函数中,系统并没有在堆区分配一个内存空间给arr2的数据成员m_iArr。所以这里两个对象的数据成员m_iArr显然都指向了同一块内存空间,那么会有什么问题呢? 
当我们在调用析构函数,释放内存空间的时候,两个对象指向的那块内存空间就会被释放两次,这样程序会奔溃,导致出错。 
这就是浅拷贝带来的危险 
这里再引入深拷贝:

Array(const Array &arr){ /// 拷贝构造函数
      m_iCount = arr.m_iCount;
      m_pArr = new int[m_iCount];
      for(int i=0;i<m_iCount;i++) m_pArr[i]=arr.m_pArr[i];
        cout<<"Array &"<<endl;
    }

当类中存在数据成员需要动态开辟内存空间的时候,需要使用深拷贝的方式

区别: 
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

  深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。


猜你喜欢

转载自blog.csdn.net/coolwriter/article/details/80558091