现在的位置: 首页 > 综合 > 正文

深拷贝和浅拷贝

2013年09月16日 ⁄ 综合 ⁄ 共 2314字 ⁄ 字号 评论关闭

在android中,偶尔看到

DragController dragController = mDragController;

final Workspace workspace = mWorkspace;

引发的追根溯源

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~浅拷贝与深拷贝

用C++编程,我们经常用到这样的方法。
class A{
….
}

A a1;
A a2 = a1;
A a3 = A(a1);
上面的代码就涉及到了对象的内存拷贝问题。
我们都知道,在C++中,有四个特殊的函数,如果在类定义中没有声明,那么C++编译器会自动在类中加入这四个函数的定义,它们分别是构造函数,析构函数,拷贝构造函数和拷贝赋值函数。上面的代码, A a2=a1; 实际上调用的拷贝赋值函数,A a3= A(a1);用到的是拷贝构造函数。C++默认提供的这两个函数的实现是按位拷贝,如果一个类的数据成员中没有指针对象。这两个默认的函数实现能够满足上面代码的需求,不会出现任何问题。但如果一个类的声明如下
class A{
public:

int mIdx;
char* mpC;
}
那么在执行A a2=a1或者 A a3=A(a1)这样的代码的时候,就要小心了,数据成员mIdx 和 mpC在对象a1,a2,a3个有一份拷贝,但是mpC 指向同一块内存区,而不是在a1,a2,a3中各有一份。那么当 执行*(a1.mpC) ++ 时,a2,a3中mpC指向的数据同样会改变,这也就是我们通常说的浅拷贝,要想避免这种问题,开发人员需要手动编写拷贝构造函数和拷贝赋值函数,载这两个函数中,为mpC分配相应的内存.
A::A(const A& a)
{
mIdx = a.mIdx;
int len = strlen(a.mpC);
mpC = new char[len];
memcopy(mpC,a.mpC,len)
….
}
这种实现也就是大家通常说的深拷贝,问题说到这里,深拷贝和浅拷贝的事情是不是已经清楚了呢? 也许有人会认为,既然浅拷贝给程序带来Bug,直接定下规则,写程序的时候注意,不要对象的浅拷贝出现。简单地说,浅拷贝没有好处,只有坏处,应该避之唯恐不及。
这也是我自己最开始的想法,在开发过程中,如果类中定义中用到了指针,就为这个类实现深拷贝构造函数和拷贝赋值函数。
随着开发的深入,渐渐发现,浅拷贝也是有用处的,而且有很大用处。
现成的例子就是Qt, 那个C++的图形类库,想来很多对这个名称并不陌生,Linux下的KDE就是基于Qt 开发的。
Qt类库提供了一个字符串类,QString, 它的设计就专门用到了浅拷贝。举个例子
QString s1 = QString(“123456789”); //这是深拷贝,因为调用的是构造函数。
QString s2 = s1; //浅拷贝,同样指向了 “123456789”
QString s3 = QString(s1); //浅拷贝,同样指向了 “123456789”

这就有问题了,既然是浅拷贝,如果执行下面的操作
QString s4 = QString(“0”);
s1.append(s4);
s1的内容就会成为”1234567890”,那么,s2,s3的内容也会变成”1234567890”呢,答案是不会,因为在QString的设计中,把深拷贝延后了,如果用户程序只对s1,s2,s3做查找的操作,不涉及字符串内容的拷贝,那么s1,s2,s3内部数据都指向同一块内存区,但如果涉及到字符串的改变,比如,转换成大写,小写,增加,剪切等,属于这个对象的数据要先做一份深拷贝,然后再做相应的操作。
就象上面的代码,s2,s3还是指向原来的内存区域,而s1已经是另外一块内存区域了。QString 这样的设计,是基于用户对字符串的操作,查找多于改变的这个前提。因为并不是在每次赋值的时候拷贝整个字符串,可以提高程序的性能。这也是设计模式中proxy模式的一种另类实现吧。
不过,这也带来一个很有趣的问题,研究下面两个代码:

void getString(QString& s1)
{
QString s = QString(“123456789”);
s1 =s;
}

int main()
{
QString s1;
getString(s1);
QString s2 = QString(“0”);
s1.append(s2);
}
问题是,当执行 s1.append(s2)后,拷贝了”123456789”的内容,然后append “0”变成”1234567890”,那么原来的”123456789”什么时候释放呢?
写在QString的析构函数里肯定是错误的,因为QString s1是局部变量,在执行函数getString()的最后,就要调用一次QString的析构函数,如果直接写在这里,s2的内容就不会是”123456789”了。
那么写在append函数里怎么样? 一乍看,有道理,先拷贝一份内存,然后销毁内存区”123456789”。但如果代码这样写,这种简单的实现就有问题了。

int main()
{
QString s1,sx;
getString(s1);
getString(sx);
QString s2 = QString(“0”);
s1.append(s2);
sx.append(s2);
}

如果执行append() 函数,”123456789”内存区就被直接销毁,那么接下来sx.append(s2)就要出错,因为这时sx还是指向“123456789”内存区的。

具体的实现方法很简单,引入一个计数器,记录内存”12345679”被引用了多少次,每个对象的创建,如果是引用这块内存,计数器相应增加,对象销毁的时候,看计数器是否为零,如果不是,只减少计数器的值,等计数器值为零时,才做真正的内存销毁。

抱歉!评论已关闭.