文章出处:http://epubcn.rubypdf.com/simple/index.php?t59868.html
用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];
memcpy(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"了。
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"内存区的。
具体的实现方法很简单,引入一个计数器,记录内存"123456789"被引用了多少次,每个对象的创建,如果是引用这块内存,计数器相应增加,对象销毁的时候,看计数器是否为零,如果不是,只减少计数器的值,等计数器的值为零时,才做真正的内存销毁。
QString用了一种很简洁的设计,来实现这种内存管理的方法,代码写的很有水平,有兴趣的话可以看看Qt的源码,这是从网上可以免费下载的。
Qt的网址http://www.trolltech.com/
von | 2006-03-15 13:53 |
Downloads and Evaluations ? |
lxgbrian | 2006-03-15 15:35 |
有GNU版的。 |
cateyes | 2006-05-05 15:30 |
有一点应该是不对的. A a2 = a1; 这个调用的还是拷贝构造函数, 而不是拷贝赋值函数. 它与A a3 = A(a1); 完全等价. 所以在C++中提倡用后一形式, 而不是前一形式. |