qt - 隐式共享与d-pointer技术

文章介绍了Qt库中广泛使用的隐式共享技术,通过共享内存来节省空间,提高程序运行速度。d-pointer模式在此中起到关键作用,允许类的数据成员在多对象间共享,同时保持对象间的独立性。文章还探讨了d-pointer在二进制代码兼容性方面的应用,以及如何在QObject中实现d-pointer模式。


前言

一般情况下,一个类的多个对象所占用的内存是相互独立的。如果其中某些对象数据成员的取值完全相同,我们可以令它们共享一块内存以节省空间。只有当程序需要修改其中某个对象的数据成员时,我们再为该对象分配新的内存。这种技术被称为隐式共享(implicit sharing)。该技术被Qt库广泛使用,接下来,介绍该技术,并剖析QString的部分源代码以演示该技术的具体实现。

通常情况下,与一个类密切相关的数据会被作为数据成员直接定义在该类中。然而,在某些场合下,我们会将这些数据从该类(被称为公类)分离出来,定义在一个单独的类中(被称为私类)。公类中会定义一个指针,指向私类的对象。在计算机的发展历史中,这种模式被称为pointer to implementation (pimpl),handle/body或者cheshire cat。这种模式起初由Trolltech公司的职员Arnt Gulbrandsen引入到Qt中。由于这个指针实质上指向了一个类的数据,Qt程序员常将其命名为d_ptr或者d。Qt文档以及本书将其称为d-pointer。

d-pointer模式具有多种用途。第2节讨论d-pointer在隐式共享中的作用。第3节介绍如何使用d-pointer维持Qt库的二进制兼容性。第4节给出d-pointer模式的具体实现。QObject的许多派生类都利用了d-pointer模式,第5介绍如何在QObject中实现该模式,以供其所有的派生类共享这个功能。

1. 隐式共享

隐式共享(implicit sharing)的目的在于节省内存、提高程序运行速度。如图1所示,设对象O1、O2、O3的部分数据成员具有相同的取值。为了节省内存,我们用一个内存块A来存放这些数据成员,每个对象内部有一个指针,指向这个内存块。设此时有一个O3来复制构造O4的操作。由于O4和O3此时具有相同的数据成员,所以O4也可以和O1~O3共享内存块A。此后,程序请求修改O4的数据成员。由于逻辑上O1、O2、O3、O4是相互独立的对象,所以我们不能够直接修改内存块A中的数据,否则,会影响O1~O3的数据。内存块A中的数据被复制到一个新的内存块B,对O4的修改会施加到内存块B中存放的数据。这就是所谓“写时复制(copy-on-write)”名称的来源。这种技术能够在逻辑上保证各个对象是相互独立的。同时,在物理实现上,只要某些对象的数据成员值相同,则它们就会共享内存,以节省内存资源。

在这里插入图片描述
图1 隐式共享技术
除了能够节省内存之外,这种技术还可以提高程序运行速度。设想我们需要用O3来构造一个对象O5。如果O3、O5各自使用独立的内存块来存放数据,则这个构造操作需要将O3中的数据完全复制到O5中,这需要较长的运行时间。而采用了隐式共享技术后,只需要设置O5中的一个指针,以指向被共享的数据块,这个操作的执行速度会很快。

当然,采用这种技术需要一些额外的内存管理工作。在图1中,设想客户要析构对象O3。我们不能够简单地将O3所用的内存块A释放,因为O1、O2还在使用这个内存块。当只有一个对象使用内存块A时,在析构该对象时才可以释放内存块A。为此,我们需要维护一个引用计数(reference counter)。每当有一个新的对象需要共享该内存块时,该内存块对应的引用计数被加1,每当共享该内存块的一个对象被析构时,该引用计数被减1。如果减1后为0,则说明已经没有任何对象需要使用该内存块,该内存块被释放

我们以QString为例来剖析隐式共享的实现。QString有一个成员函数toCaseFolded(),能够将一个字符串中的所有字符变为小写的。设有下面的语句:

      QString s1 ="HELLO world!";
      QString s2 = s1.toCaseFolded();

这两行执行完后,s2中的内容为“hello world!”,而s1的内容不变,仍然为“HELLO world!”。由于s1和s2具有不同的数据内容,因而必须为s2分配独立的存储空间来存放修改后的字符数据。然而,如果s1中的字符串本身已经为小写的“hello world!”,转换后的字符串不变,我们就可以让s2和s1共享一个内部数据区。

成员函数toCaseFolded()采用了这一技术,其源代码如代码段1所示。行①的d是QString的成员变量,指向一块内存,其中存放着QString对象所有的属性信息。也就是说,QString并没有直接使用它的成员变量来存放其属性信息,这种技术被称为d-pointer模式,第2节将详细讨论这种技术。d->size表示字符串的长度。值为0表示空串,无须转换,直接将当前对象返回。

代码段1,采用隐式共享技术的QString::toCaseFolded( )

      QString QString::toCaseFolded() const
      {
   
   
            if (!d->size)return *this;const ushort *p = d->data;
            if (!p) return *this;
            const ushort *e = d->data + d->size;
            uint last = 0;
            while (p < e) {
   
   
              ushort folded = foldCase(*p, last);if (folded != *p) {
   
                    ③
                    QString s(*this);            ④
                    s.detach();                   ⑤
                    ushort *pp = s.d->data + (p - d->data);
                    const ushort *ppe = s.d->data + s.d->size;
                    last = pp > s.d->data ? *(pp -1) : 0;
                    while (pp < ppe) {
   
   
            			*pp = foldCase(*pp, last);
            			++pp;
                    }
                    return s;}
              p++;
            }
            return *this;
      }

行②求取当前对象中某一个字符的小写形式,行③判断该小写形式是否和原字符相同。如果不同,则说明成员函数toCaseFolded()将要返回的对象和当前对象具有不同的字符数据,因而无法再和当前对象共享内存块。

行④用当前对象创建一个新的对象s,这调用了QString的复制构造函数,如代码段8-2所示。行⑦使得当前对象和新对象的d-pointer指向同一块内存,导致两个对象共享相同的字符串数据区。而行⑧增加一个引用计数,表示有了一个新对象s需要引用字符串数据区。

代码段2,QString的复制构造函数

      inline QString::QString(const QString &other) : d(other.d)  (7)
      {
   
   
      	Q_ASSERT(&other != this);
      	d
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值