在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”被引用了多少次,每个对象的创建,如果是引用这块内存,计数器相应增加,对象销毁的时候,看计数器是否为零,如果不是,只减少计数器的值,等计数器值为零时,才做真正的内存销毁。


更多相关文章

  1. android 监听网络状态
  2. Android中的BroadCast简单使用
  3. Android(安卓)解锁屏启动过程
  4. Android(安卓)onItemLongClick+onCreateContextMenu setOnCreateC
  5. Android访问网络常见问题之一
  6. 如何在JNI中抛异常
  7. tab使用 TabActivity TabHost Tabspec常用方法
  8. android JNI 多线程 C函数回调
  9. 编写android加载图片的程序时,遇到了内存泄露问题!

随机推荐

  1. android事件拦截处理机制详解
  2. Android(安卓)反编译apk 到java源码的方
  3. Android是个好系统
  4. Android(安卓)多点触控技术
  5. Android自定义扁平化对话框
  6. 一个用于Android的Web服务器
  7. [置顶] 关于Android分辨率的支持(转)
  8. Android自定义进度条
  9. Android移植成功:linux-2.6.25.8+U盘挂载
  10. [置顶] Android(安卓)App关于应用程序升