深拷贝、浅拷贝构造函数

Trend科技的一道面试题:

请看下面的程序,说说会出现什么问题?

#include <iostream>
#include <cstdlib>
#include <vector>  
using   namespace   std;  

class   CDemo   {  
public:  
    CDemo():str(NULL){};  
    ~CDemo()   
    {   
        if(str)   delete[]   str;   
    };  
    char*   str;  
};  

int   main(int   argc,   char**   argv)   {  
    CDemo   d1;  
    d1.str=new   char[32];  
    strcpy(d1.str, "trend   micro");  

    vector<CDemo>   *a1=new   vector<CDemo>();  
    
    a1->push_back(d1);
    delete   a1;
  
    return EXIT_SUCCESS;
}

这个程序在退出时,会出问题,什么问题?重复delete同一片内存,程序崩溃。
我们把析构函数改为如下,可以更清楚的看到这一点:
    ~CDemo() 
    { 
        if(str)
        {
            static int i=0;
            cout<<"&CDemo"<<i++<<"="<<(int*)this<<",    str="<<(int *)str<<endl;
            delete[]   str;    
        }
    };
  
运行时我们发现打印如下信息:
&CDemo0=000309D8,       str=000307A8
&CDemo1=0013FF70,       str=000307A8
也就是说,发生了CDemo类的两次析构,两次析构str所指向的同一内存地址空间(两次str值相同=000307A8)。
为什么?

《程序员面试宝典》第二版,P99,有句解释“vector对象指针能够自动析构,所以不需要调用delete a1,否则会造成两次析构对象”

我切以为这句话说的有点不妥。任何对象如果是通过new操作符申请了空间,必须显示的调用delete来销毁这个对象。所以“delete   a1;  ”这条语句是没有错误的。
这句话“vector<CDemo>   *a1=new   vector<CDemo>();  ”定一个指针,指向 vector<CDemo>,病用new操作符进行了初始化, 我们必须在适当的时候释放a1所占的内存空间,所以“delete   a1;  ”这句话是没有错误的。另外,我们必须明白一点,释放vector对象,vector所包含的元素也同时被释放。

那到底那里错误?

这句a1的声明和初始化语句“vector<CDemo>   *a1=new   vector<CDemo>();  ”说明a1所含元素是“CDemo”类型的,在执行“a1->push_back(d1);  ”这条语句时,会调用CDemo的拷贝构造函数,虽然CDemo类中没有定义拷贝构造函数,但是编译器会为CDemo类构建一个默认的拷贝构造函数(浅拷贝),这就好像任何对象如果没有定义构造函数,编译器会构建一个默认的构造函数一样。

正是这里出了问题。a1中的所有CDemo元素的str成员变量没有初始化,只有一个四字节(32位机)指针空间。
“a1->push_back(d1);”这句话执行完后,a1里的CDemo元素与d1是不同的对象,但是a1里的CDemo元素的str与d1.str指向的是同一块内存,这从后来的打印信息就可以看出来。

我们知道,局部变量,如“CDemo   d1;  ” 在main函数退出时,自动释放所占内存空间,
那么会自动调用CDemo的析构函数“~CDeme”,问题就出在这里。

前面的“delete   a1;”已经把 d1.str 释放了(因为a1里的CDemo元素的str与d1.str指向的是同一块内存),main函数退出时,又要释放已经释放掉的 d1.str 内存空间,所以程序最后崩溃。



解释清楚了。
这里最核心的问题归根结底就是浅拷贝和深拷贝的问题。如果CDemo类添加一个这样的拷贝构造函数就可以解决问题:
    CDemo(const   CDemo   &cd)
    {
        this->str   =   new   char[strlen(cd.str)+1];
        strcpy(str,cd.str);
    };
这就是深拷贝。

或者这样用:
    vector<CDemo*>   *a1=new   vector<CDemo*>();
    a1->push_back(&d1);
那么在    “delete   a1;” a1释放,同时a1里面包含的元素(”CDemo*“类型,仍然是一个指针,4字节空间)。
深拷贝,浅拷贝:
浅拷贝就是成员数据之间的一一赋值:把值赋给一一赋给要拷贝的值。但是可能会有这样的情况:对象还包含资源,这里的资源可以值堆资源,或者一个文件。。当值拷贝的时候,两个对象就有用共同的资源,同时对资源可以访问,这样就会出问题。深拷贝就是用来解决这样的问题的,它把资源也赋值一次,使对象拥有不同的资源,但资源的内容是一样的。对于堆资源来说,就是在开辟一片堆内存,把原来的内容拷贝。 
如果你拷贝的对象中引用了某个外部的内容(比如分配在堆上的数据),那么在拷贝这个对象的时候,让新旧两个对象指向同一个外部的内容,就是浅拷贝;如果在拷贝这个对象的时候为新对象制作了外部对象的独立拷贝,就是深拷贝 
引用和指针的语义是相似的,引用是不可改变的指针,指针是可以改变的引用。其实都是实现了引用语义。 
深拷贝和浅拷贝的区别是在对象状态中包含其它对象的引用的时候,当拷贝一个对象时,如果需要拷贝这个对象引用的对象,则是深拷贝,否则是浅拷贝。 
COW语义是“深拷贝”与“推迟计算”的组合,仍然是深拷贝,而非浅拷贝,因为拷贝之后的两个对象的数据在逻辑上是不相关的,只是内容相同。 
无论深浅,都是需要的。当深拷贝发生时,通常表明存在着一个“聚合关系”,而浅拷贝发生时,通常表明存在着一个“相识关系”。 
举个简单的例子: 
当你实现一个Composite  Pattern,你通常都会实现一个深拷贝(如果需要拷贝的话),很少有要求同的Composite共享Leaf的; 
而当你实现一个Observer  Pattern时,如果你需要拷贝Observer,你大概不会去拷贝Subject,这时就要实现个浅拷贝。 
是深拷贝还是浅拷贝,并不是取决于时间效率、空间效率或是语言等等,而是取决于哪一个是逻辑上正确的
发布了22 篇原创文章 · 获赞 25 · 访问量 19万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章