深拷贝与浅拷贝探析

来源:互联网 发布:淘宝网站设计模板 编辑:程序博客网 时间:2024/06/05 02:01

                                                    深拷贝与浅拷贝探析
1.         深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。举个例子,一个人名叫张三,后来用他克隆(假设法律允许)了另外一个人,叫李四,不管是张三缺胳膊少腿还是李四缺胳膊少腿都不会影响另外一个人。比较典型的就是Value(值)对象,如预定义类型Int32,Double,以及结构(struct),枚举(Enum)等。

考虑以下写法

       int source = int.MaxValue;//(1)初始化源对象为整数的最大值2,147,483,647

            int dest = source;//(2)赋值,内部执行深拷贝

            dest = 1024;//(3)对拷贝对象进行赋值

            source = 2048;//(4)对源对象进行赋值

       首先(2)中将source赋给dest,执行了深拷贝动作,其时dest和source的值是一样的,都是int.MaxValue;(3)对dest进行修改,dest值变为1024,由于是深拷贝,因此不会运行source,source仍然是int.MaxValue;(4)对source进行了修改,同样道理,dest仍然是1024,同时int.MaxValue的值也不变,仍然是2,147,483,647;只有source变成了2048。

       再考虑以下写法

        struct Point

        {

            public int X;

            public int Y;

            public Point(int x, int y)

            {

                X = x;

                Y = y;

            }

        }

 

        Point source = new Point(10, 20);

        Point dest = source;

 

        dest.X = 20

     当dest.X属性变成20后,source的X属性仍然是10

2.         浅拷贝是指源对象与拷贝对象共用一份实体,仅仅是引用的变量不同(名称不同)。对其中任何一个对象的改动都会影响另外一个对象。举个例子,一个人一开始叫张三,后来改名叫李四了,可是还是同一个人,不管是张三缺胳膊少腿还是李四缺胳膊少腿,都是这个人倒霉。比较典型的就有Reference(引用)对象,如Class(类)。

考虑以下写法

        class Point

        {

            public int X;

            public int Y;

            public Point(int x, int y)

            {

                X = x;

                Y = y;

            }

        }

 

        Point source = new Point(10, 20);

        Point dest = source;

   dest.X = 20;

由于Point现在是引用对象,因此Point dest=source的赋值动作实际上执行的是浅拷贝,最后的结果应该是source的X字段值也变成了20。即它们引用了同一个对象,仅仅是变量明source和dest不同而已。

3.         引用对象的浅拷贝原理

引用对象之间的赋值之所以执行的是浅拷贝动作,与引用对象的特性有关,一个引用对象一般来说由两个部分组成

(1)一个具名的Handle,也就是我们所说的声明(如变量)

(2)一个内部(不具名)的对象,也就是具名Handle的内部对象。它在Manged Heap(托管堆)中分配,一般由新增引用对象的New方法是进行创建

       如果这个内部对象已被创建,那么具名的Handle就指向这个内部对象在Manged Heap中的地址,否则就是null(从某个方面来讲,如果这个具名的handle可以被赋值为null,说明这是一个引用对象,当然不是绝对)。两个引用对象如果进行赋值,它们仅仅是复制这个内部对象的地址,内部对象仍然是同一个,因此,源对象或拷贝对象的修改都会影响对方。这也就是浅拷贝

4.         引用对象如何进行深拷贝

由于引用对象的赋值仅仅是复制具名Handle(变量)指向的地址,因此要对引用对象进行深拷贝就要重新创建一份该对象的实例,并对该对象的字段进行逐一赋值,如以下写法

        class Point

        {

            public int X;

            public int Y;

            public Point(int x, int y)

            {

                X = x;

                Y = y;

            }

        }

 

        Point source = new Point(10, 20);

        Point dest = new Point(source.X, source.Y);

        //或以下写法

        //Point dest = new Point()

        //dest.X = source.X

   //dest.Y = source.Y

       其时,source和dest就是两个互相独立的对象了,两者的修改都不会影响对方

 

5.一些需要注意的东西

       (1):String字符串对象是引用对象,但是很特殊,它表现的如值对象一样,即对它进行赋值,分割,合并,并不是对原有的字符串进行操作,而是返回一个新的字符串对象

       (2):Array数组对象是引用对象,在进行赋值的时候,实际上返回的是源对象的另一份引用而已;因此如果要对数组对象进行真正的复制(深拷贝),那么需要新建一份数组对象,然后将源数组的值逐一拷贝到目的对象中







浅拷贝就是成员数据之间的一一赋值:把值赋给一一赋给要拷贝的值。但是可能会有这样的情况:对象还包含资源,这里的资源可以值堆资源,或者一个文件。。当 值拷贝的时候,两个对象就有用共同的资源,同时对资源可以访问,这样就会出问题。深拷贝就是用来解决这样的问题的,它把资源也赋值一次,使对象拥有不同的 资源,但资源的内容是一样的。对于堆资源来说,就是在开辟一片堆内存,把原来的内容拷贝。  

如果你拷贝的对象中引用了某个外部的内容(比如分配在堆上的数据),那么在拷贝这个对象的时候,让新旧两个对象指向同一个外部的内容,就是浅拷贝;如果在拷贝这个对象的时候为新对象制作了外部对象的独立拷贝,就是深拷贝  

引用和指针的语义是相似的,引用是不可改变的指针,指针是可以改变的引用。其实都是实现了引用语义。  
深拷贝和浅拷贝的区别是在对象状态中包含其它对象的引用的时候,当拷贝一个对象时,如果需要拷贝这个对象引用的对象,则是深拷贝,否则是浅拷贝。  

COW语义是“深拷贝”与“推迟计算”的组合,仍然是深拷贝,而非浅拷贝,因为拷贝之后的两个对象的数据在逻辑上是不相关的,只是内容相同。  

无论深浅,都是需要的。当深拷贝发生时,通常表明存在着一个“聚合关系”,而浅拷贝发生时,通常表明存在着一个“相识关系”。  
举个简单的例子:  
当你实现一个Composite  Pattern,你通常都会实现一个深拷贝(如果需要拷贝的话),很少有要求同的Composite共享Leaf的;  
而当你实现一个Observer  Pattern时,如果你需要拷贝Observer,你大概不会去拷贝Subject,这时就要实现个浅拷贝。  
是深拷贝还是浅拷贝,并不是取决于时间效率、空间效率或是语言等等,而是取决于哪一个是逻辑上正确的

//--------------------------------------------------------------------------------

在学习这一章内容前我们已经学习过了类的构造函数和析构函数的相关知识,对于普通类型的对象来说,他们之间的复制是很简单的,例如:

int a = 10; int b =a;

自己定义的类的对象同样是对象,谁也不能阻止我们用以下的方式进行复制,例如:

#include <iostream>usingnamespacestd; classTest { public: Test(inttemp) { p1=temp; } protected: intp1; }; voidmain() { Test a(99); Test b=a; }

普通对象和类对象同为对象,他们之间的特性有相似之处也有不同之处,类对象内部存在 成员变量,而普通对象是没有的,当同样的复制方法发生在不同的对象上的时候,那么系统对他们进行的操作也是不一样的,就类对象而言,相同类型的类对象是通 过拷贝构造函数来完成整个复制过程的,在上面的代码中,我们并没有看到拷贝构造函数,同样完成了复制工作,这又是为什么呢?因为当一个类没有自定义的拷贝 构造函数的时候系统会自动提供一个默认的拷贝构造函数,来完成复制工作。

下面,我们为了说明情况,就普通情况而言(以上面的代码为例),我们来自己定义一个与系统默认拷贝构造函数一样的拷贝构造函数,看看它的内部是如何工作的!

代码如下:

#include <iostream>usingnamespacestd; classTest { public: Test(inttemp) { p1=temp; } Test(Test &c_t)//这里就是自定义的拷贝构造函数 { cout<<"进入copy构造函数"<p1=c_t.p1;//这句如果去掉就不能完成复制工作了,此句复制过程的核心语句 } public: intp1; }; voidmain() { Test a(99); Test b=a; cout<cin.get(); }

上面代码中的Test(Test &c_t)就是我们自定义的拷贝构造函数,拷贝构造函数的名称必须与类名称一致,函数的形式参数是本类型的一个引用变量,且必须是引用。

当用一个已经初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝 构造函数就会被自动调用,如果你没有自定义拷贝构造函数的时候系统将会提供给一个默认的拷贝构造函数来完成这个过程,上面代码的复制核心语句就是通过 Test(Test &c_t)拷贝构造函数内的p1=c_t.p1;语句完成的。如果取掉这句代码,那么b对象的p1属性将得到一个未知的随机值;

下面我们来讨论一下关于浅拷贝和深拷贝的问题。

就上面的代码情况而言,很多人会问到,既然系统会自动提供一个默认的拷贝构造函数来 处理复制,那么我们没有意义要去自定义拷贝构造函数呀,对,就普通情况而言这的确是没有必要的,但在某写状况下,类体内的成员是需要开辟动态开辟堆内存 的,如果我们不自定义拷贝构造函数而让系统自己处理,那么就会导致堆内存的所属权产生混乱,试想一下,已经开辟的一端堆地址原来是属于对象a的,由于复制 过程发生,b对象取得是a已经开辟的堆地址,一旦程序产生析构,释放堆的时候,计算机是不可能清楚这段地址是真正属于谁的,当连续发生两次析构的时候就出 现了运行错误。

为了更详细的说明问题,请看如下的代码。

#include <iostream>usingnamespacestd; classInternet { public: Internet(char*name,char*address) { cout<<"载入构造函数"<strcpy(Internet::name,name); strcpy(Internet::address,address); cname=newchar[strlen(name)+1]; if(cname!=NULL) { strcpy(Internet::cname,name); } } Internet(Internet &temp) { cout<<"载入COPY构造函数"<strcpy(Internet::name,temp.name); strcpy(Internet::address,temp.address); cname=newchar[strlen(name)+1];//这里注意,深拷贝的体现! if(cname!=NULL) { strcpy(Internet::cname,name); } } ~Internet() { cout<<"载入析构函数!"; delete[] cname; cin.get(); } voidshow(); protected: charname[20]; charaddress[30]; char*cname; }; voidInternet::show() { cout<} voidtest(Internet ts) { cout<<"载入test函数"<} voidmain() { Internet a("中国软件开发实验室","www.cndev-lab.com"); Internet b =a; b.show(); test(b); }

上面代码就演示了深拷贝的问题,对对象b的cname属性采取了新开辟内存的方式避免了内存归属不清所导致析构释放空间时候的错误,最后我必须提一下,对于上面的程序我的解释并不多,就是希望读者本身运行程序观察变化,进而深刻理解。

深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源但复制过程并未复制资源的情况视为浅拷贝。

浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错,这点尤其需要注意!


以前我们的教程中讨论过函数返回对象产生临时变量的问题,接下来我们来看一下在函数中返回自定义类型对象是否也遵循此规则产生临时对象!

先运行下列代码:

#include <iostream>usingnamespacestd; classInternet { public: Internet() { }; Internet(char*name,char*address) { cout<<"载入构造函数"<strcpy(Internet::name,name); strcpy(Internet::address,address); } Internet(Internet &temp) { cout<<"载入COPY构造函数"<strcpy(Internet::name,temp.name); strcpy(Internet::address,temp.address); cin.get(); } ~Internet() { cout<<"载入析构函数!"; cin.get(); } protected: charname[20]; charaddress[20]; }; Internet tp() { Internet b("中国软件开发实验室","www.cndev-lab.com"); returnb; } voidmain() { Internet a; a=tp(); }

从上面的代码运行结果可以看出,程序一共载入过析构函数三次,证明了由函数返回自定义类型对象同样会产生临时变量,事实上对象a得到的就是这个临时Internet类类型对象temp的值。

这一下节的内容我们来说一下无名对象。

利用无名对象初始化对象系统不会不调用拷贝构造函数。

那么什么又是无名对象呢?

很简单,如果在上面程序的main函数中有:

Internet ("中国软件开发实验室","www.cndev-lab.com");

这样的一句语句就会产生一个无名对象,无名对象会调用构造函数但利用无名对象初始化对象系统不会不调用拷贝构造函数!

下面三段代码是很见到的三种利用无名对象初始化对象的例子。

#include <iostream>usingnamespacestd; classInternet { public: Internet(char*name,char*address) { cout<<"载入构造函数"<strcpy(Internet::name,name); } Internet(Internet &temp) { cout<<"载入COPY构造函数"<strcpy(Internet::name,temp.name); cin.get(); } ~Internet() { cout<<"载入析构函数!"; cin.get(); } public: charname[20]; charaddress[20]; }; voidmain() { Internet a=Internet("中国软件开发实验室","www.cndev-lab.com"); cout<cin.get(); }

上面代码的运行结果有点“出人意料”,从思维逻辑上说,当无名对象创建了后,是应该调用自定义拷贝构造函数,或者是默认拷贝构造函数来完成复制过程的,但事实上系统并没有这么做,因为无名对象使用过后在整个程序中就失去了作用,对于这种情况c++会把代码看成是:

Internet a("中国软件开发实验室","www.cndev-lab.com");

省略了创建无名对象这一过程,所以说不会调用拷贝构造函数。

最后让我们来看看引用无名对象的情况。

#include <iostream>usingnamespacestd; classInternet { public: Internet(char*name,char*address) { cout<<"载入构造函数"<strcpy(Internet::name,name); } Internet(Internet &temp) { cout<<"载入COPY构造函数"<strcpy(Internet::name,temp.name); cin.get(); } ~Internet() { cout<<"载入析构函数!"; } public: charname[20]; charaddress[20]; }; voidmain() { Internet &a=Internet("中国软件开发实验室","www.cndev-lab.com"); cout<cin.get(); }

引用本身是对象的别名,和复制并没有关系,所以不会调用拷贝构造函数,但要注意的是,在c++看来:

Internet &a=Internet("中国软件开发实验室","www.cndev-lab.com");

是等价与:

Internet a("中国软件开发实验室","www.cndev-lab.com");

的,注意观察调用析构函数的位置(这种情况是在main()外调用,而无名对象本身是在main()内析构的)。



原创粉丝点击