深拷贝和浅拷贝

    文章出处: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/

 

von2006-03-15 13:53
Downloads and Evaluations ?

lxgbrian2006-03-15 15:35
有GNU版的。

cateyes2006-05-05 15:30
有一点应该是不对的.

A a2 = a1; 这个调用的还是拷贝构造函数, 而不是拷贝赋值函数. 它与A a3 = A(a1); 完全等价. 所以在C++中提倡用后一形式, 而不是前一形式.

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值