深拷贝、浅拷贝基础:http://blog.csdn.net/songshimvp1/article/details/48244599
*******************************************************************************************************************************
大家都知道,C++中类的默认的拷贝构造函数是按位拷贝的,也就是直接将一个对象的成员值拷贝过来;
比如一个类A,我们不显示提供拷贝构造函数的话:
如下:
<span style="font-size:18px;">class
{
int a;
char arr[10];
char *p;
};
A a1;
A a2 = a1;</span>
这个时候,a2和a1的成员 int a和arr[ ]是相同的值,成员是独立的,修改某一个的成员不会影响另外一个,但是这个时候要注意的是指针p;
虽然我们输出的结果一样,但是不能草率下决定。因为这个时候a2和a1的p值是一样的,也就是说两个指针指向同一个地方;
所以这样的拷贝会经常出现问题,比如两次释放指针的内存等。解决办法大家都知道是自己构造一个按内容拷贝的拷贝构造函数,这里我也就不多说了。
我想讨论的问题是如下的问题:
C++编译器在什么情况下不会按照默认的Bitwise copy来做呢?《深度探索C++对象模型》P53
首先有两种情况我不想说,
1:就是含有的成员对象本身提供了拷贝构造函数(不管是默认的还是自己提供的),这个时候当拷贝这个对象的时候调用的是对象的类提供的拷贝构造函数。
2:继承的基类有拷贝构造函数,这个时候编译器会插入基类的拷贝构造函数,而不是编译器自己来提供。这两种都不会按照默认的拷贝语意来做。
我想讨论下面两种:
3:含有虚函数的类;——(reset)重新设定virtual table的指针
考虑下面的代码:
class Foo
{
public:
int a;
Foo *next;
virtual void Func(void) { cout << "call Foo::func()" << endl; }
};
class D :public Foo
{
public:
int c;
virtual void Func(void) { cout << "call D::func()" << endl; }
};
int main(void)
{
D d;
Foo f = d;
d.Func();
f.Func();
system("pause");
return 0;
}
输出的结果是什么呢?运行看看:结果完全按照我们想要的要求运行:call D::func() call Foo::func()
所以这个就是说,当含有虚函数的时候,我们的类会有虚函数表,每个对象会有vptr。如果我们的基类只是简单的将子类的vptr值拷贝过来,显然是不符合要求的,于是这个时候编译器会给我们自动的停用bitwise拷贝,而用reset将基类的vptr指向基类的虚函数表。所以这个时候是达到了我们的要求的。我想说的是,其他值会按照默认的按位拷贝,只是vptr不会,还有我们下一个将讨论的也不会。
4:虚继承
与第三种情况类似,但是有区别,这里有vbtl,然而vbtl是采用的重新初始化,而不是reset;
让我们看看例子:
class Raccoon : public virtual ZooAnimal
{
public: Raccoon() { /* private data initialization */ }
Raccoon(int val) { /* private data initialization */ }
// ... private: // all necessary data
};
class RedPanda : public Raccoon
{
public: RedPanda() { /* private data initialization */ }
RedPanda(int val) { /* private data initialization */ }
// ... private: // all necessary data
};
这里如果是两个一样的类的对象间进行拷贝,简单的按位拷贝就会解决问题,而我们的问题在于父类与子类之间的拷贝;
如: RedPanda little_red;
Raccoon little_critter = little_red;
这个时候,编译器会在默认的拷贝构造函数中插入初始化指向虚基类的指针,而不是reset。这个与类的内部存储结构有关,建议看看之后便一目了然了。
*******************************************************************************************************************************************************************
memberwise copy和bitwise copy
首先说一下深拷贝(memberwise copy)和浅拷贝(bitwise copy)的问题。一般来说,自己定义的copy ctor对于对象的拷贝会有严格的、符合语义的定义(人为错误、破坏因素除外)。然而,无论是自定义的还是默认的ctor,编译器都会插入对虚拟机制的处理代码,这就保证对象切片和拷贝正确的发生——可能会出乎你的意料,但符合C++的语法语义。
虚拟机制与拷贝方式
当类中没有虚拟机制、没有其他类对象的成员时(只包含built-in类型、指针或者数组),默认copy ctor进行的是bitwise copy,这会导致对象切片的发生。然而,当类中有虚拟机制,或者有其他类对象成员时,默认copy ctor采用的是memberwise copy,并且会对虚拟机制进行正确的拷贝。
因为包含虚拟机制的类在定义一个对象时,编译器会向ctor中添加初始化vtable和vbaseclasstable(依赖于具体编译器)的代码,这样可以保证vtable中的内容与类型完全匹配。也就是说MyBase和DerivedMyBase有这相似的VTABLE,但不是完全相同——例如DerivedMyBase中还可以定义自己的virtual函数,这样它的VTABLE就会有更多表项。
而多态的实现是通过将函数调用解析为VTABLE中的偏移量来实现。pMB->Get()可能会被编译器解析成:
(*pMB->__vtable[Offset_of_Get])();
而当MyBase作为虚基类时,访问其中的数据成员可能就是:
pMB->__vBaseClassMyBase->b;
那么,当“aMB = aDMB;”,copy ctor会执行memberwise copy,正确的初始化aMB的VTABLE,而不是仅仅将aDMB的VTABLE拷贝过来。如果是bitwise copy,aMB对象中的VTABLE将是aDMB的,aMB.Get()调用的将是DervieMyBase定义的Get(),这显然是不符合语义和逻辑的。
总而言之
对象切片和copy ctor是一个很复杂的东西,在有虚拟机制的情况下两者是紧密结合在一起的。因为对象切片和拷贝构造函数的问题,不通过指针或者引用无法达到多态的目的。
还有一个问题是赋值拷贝的问题,这个机制更复杂,因此Lippman建议不要再虚基类中使用数据成员。C#和Java禁止了多重继承,并将interface作为一个单独的东西,消除了赋值拷贝带来的复杂性。关于赋值拷贝的问题,有机会再讨论。
*******************************************************************************
深拷贝与浅拷贝探析
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数组对象是引用对象,在进行赋值的时候,实际上返回的是源对象的另一份引用而已;因此如果要对数组对象进行真正的复制(深拷贝),那么需要新建一份数组对象,然后将源数组的值逐一拷贝到目的对象中
***************************************************************************
在网上看到很多朋友对Shallow Copy,Deep Copy,Bitwise Copy和Memberwise Copy这4者之间的关系的理解多有混淆,Stanley Lippman的“Inside the C++ Object Model”的讲解也比较晦涩难懂,玄机逸士在此来一个简单的、明确的说明:)
Java中的Clone也有浅克隆和深克隆之分,分别对应C++中的浅拷贝和深拷贝。
Shallow Copy = Bitwise Copy,Deep Copy = Memberwise Copy.
Long story short, a shallow copy only copies the binary, in memory, print of a class. A deep copy “dives into” the members, and copy their logical data. Usually, by default, it is a Shallow/Bitwise copy.
对象(地址0x200000B0)被浅拷贝到另外一个对象(地址0x200000C0),由于其中含有一个字符串对象,其地址为0x200000D0,在浅拷贝的时候,只是简单地将地址拷贝到新对象,因此原对象和新对象所指向的内容是一样的,都是“Hello, World”,如果新对象将该字符串进行了修改,那么原对象中对应的字符串也会发生同样的变化。而这在大部分情况下是不能接受的。