Qt维基文档翻译:D-指针,D-Pointer

本文详细介绍了C++中的d-指针设计模式,以及如何通过这种方式保持库的二进制兼容性。d-指针允许在不改变公共API类大小的情况下添加新特性,避免了因类尺寸改变导致的二进制不兼容问题。此外,还讨论了q-指针、继承d-指针的优化以及Qt库中d-指针的实现。
摘要由CSDN通过智能技术生成

内容目录

什么是d-指针

二进制兼容性 — 是什么?

什么因素会打破二进制兼容性?

为什么它会崩溃?

永远不要改变一个已经导出的C++类的尺寸

d-指针

d-指针所带来的其它好处

q-指针

继承d-指针,以达到优化目的

Qt中的d-指针

Q_D和Q_Q

Q_DECLARE_PRIVATE和Q_DECLARE_PUBLIC

 

什么是d-指针

如果 妳曾经阅读过Qt 的源代码,比如说 这个 , 妳会发现, 在其中大规模地使用了 Q_D 和 Q_Q 这两个宏。 本文章,即是为了说明这两个宏的作用。

Q_D 和 Q_Q ,这两个宏,与其它组件配合起来,组成了 一个名为 d-指针 ( 也称作 暗影指针 )的设计模式 。 在这种模式下,某个 库的实现细节 ,对于用户来说是隐藏的 。这样,当对这个库的实现细节进行变更时, 不会打破二进制兼容性。

二进制兼容性 — 是什么?

在设计像 Qt 这样的开发库的时候,有一个人们狠在意的点,就是,对于那些以动态链接方式与Qt 链接的应用程序,在Qt 库被升级/替换为另一个版本的情况下,仍然能够无需重新编译而继续正常运行。举个例子,妳的应用程序,名为 CuteApp ,是基于Qt 4.5 编译链接的。那么,妳应当能够将Qt 库从Qt 4.5 升级到Qt 4.6(通常由软件包管理器自动进行!),在升级之后,妳自己之前基于Qt 4.5 编译的CuteApp 应当仍然能够正常运行。

什么因素会打破二进制兼容性?

回过头来看,对于开发库所做的哪种改变,会使得应用程序必须重新编译才能继续工作呢?下面举个例子:

class Widget

{

// ...

private :

Rect m_geometry ;

} ;

 

class Label : public Widget

{

public :

// ...

String text () const

{

return m_text ;

}

 

private :

String m_text ;

} ;

此处,我们有一个类Widget,其中有一个成员变量geometry。我们编译Widget,并且以版本 WidgetLib 1.0 的形式发布。

对于后续的 WidgetLib 1.1 版本呢,某人想到了一个好点子,让它支持样式单功能。好嘛,我们毫不费力地加上了对应的新的成员方法,以及一个新的 数据成员 。

class Widget

{

// ...

private :

Rect m_geometry ;

String m_stylesheet ; // WidgetLib 1.1 中新加入的

} ;

 

class Label : public Widget

{

public :

// ...

String text () const

{

return m_text ;

}

 

private :

String m_text ;

} ;

我们在做了以上变更之后,发布WidgetLib 1.1。然后,会发现,之前针对WidgetLib 1.0 编译并且正常运行的CuteApp,如今竟然崩溃了!

为什么它会崩溃?

因为,加入一个新的成员变量之后,我们实际上改变了Widget 和Label 对象的尺寸。那么,这个东西狠重要吗? 是的,当妳的C++编译器生成代码时,它使用 偏移 值 (offsets)来访问对象内部的数据。

以下图表,简单地表现出了上面那些 简单数据结构 对象在内存中的布局。

Label对象在WidgetLib 1.0中的布局

Label对象在WidgetLib 1.1中的布局

m_geometry <offset 0>

m_geometry <offset 0>

- - -

m_stylesheet <offset 1>

m_text <offset 1>

- - -

- - -

m_text <offset 2>

在WidgetLib 1.0 中,Label 的text 成员,其位置在 (逻辑 上的 ) offset 1 。编译 器为该应用程序生成的代码中,对于 Label::text() 方法 ,具体上是翻译成访问应用程序内存 中label 对象的offset 1 位置。 而在WidgetLib 1.1 中,Label 的 text 成员,其位置已经移动到(逻辑 上的 )offset 2!由于应用程序本身 并未被重新编译,因此, 它仍然认为 text 是位于 offset 1 , 结果 呢,实际访问的是 stylesheet 变量!

到这个时候,一定会有一些人在想, 为什么 Label::text() 的偏移计算代码是位于CuteApp 程序的二进制文件中,而不是位于WidgetLib 的二进制文件中。原因是, Label::text() 的代码是定义于头文件中,而编译器将它 内联 了。

那么 ,如果不将 Label::text() 内联的话,情况会发生变化吗?比如 说,将 Label::text() 的定义移动到源文件中?答案 是,结果不会变化。 C++编译 器要求各个对象的尺寸在编译时和运行时保持不变。 举个例子,考虑运行时栈的压栈(winding)和弹栈(unwinding)操作,如果 妳在栈上创建一个Label 对象,那么 ,编译器会根据在编译时计算出来的Label 的尺寸来生成代码,以在栈上分配空间。由于 使用了WidgetLib 1.1,这样,在运行时,Label 的尺寸已经与编译时不同了,于是 , Label 的构造函数就会覆盖掉已有的栈数据,因而对栈造成破坏。

永远不要改变一个已经导出的C++类的尺寸

总的来说,一旦妳的库已经被发布了,那么,永远不要改变任何一个 已导出的 (也就是说,对于用户是可见的)C++类的尺寸及布局(不要改变成员变量的顺序)。C++编译器,在生成代码时,会遵循一个假设,即,在应用程序被编译完毕 之后 ,每个类的尺寸以及其中数据的顺序将不再改变。

那么,如何在不改变对象的尺寸的前提下,加入新的特性呢?

d-指针

此处使用的技巧就是,保持库中所有公有类的尺寸不变,具体手段就是,仅在公有类中储存单个指针。这个指针,指向一个私有/内部的数据结构,那个数据结构中包含着所有的数据。这个内部结构的尺寸,可以随意地减小或变大,而不会对应用程序产生任何副作用。这是因为,它的指针只会被库中的代码访问到,而从应用程序的角度来看,该对象的尺寸永远不变 - 它永远保持着一个指针的尺寸。这个指针, 就被称为 d- 指针 。

以下代码展示了此种模式的关键思想(此文章中的所有代码都未提供析构函数,但是在实际代码中,妳应当加上它们)。

widget.h

/* 由于d_ptr只是 一个指针,并且永远不会在头文件中被引用

(不然会引起编译错误),所以 WidgetPrivate不需要被包含,

只需要被前向声明。

这个类的定义可写在 widget.cpp 中,

或者 写在一个单独的文件中,例如 widget_p.h 。 */

 

class WidgetPrivate;

 

class Widget

{

// ...

Rect geometry () const ;

// ...

 

private :

WidgetPrivate * d_ptr ;

} ;

widget_p.h , 即是 widget类的私有头文件。

/* widget_p.h (_p表示私有) */

struct WidgetPrivate

{

Rect geometry ;

String stylesheet ;

} ;

widget.cpp

// 通过这句#include代码,我们就可以访问到WidgetPrivate。

#include "widget_p.h"

 

Widget:: Widget () : d_ptr( new WidgetPrivate)

{

// 创建私有数据

}

 

Rect Widget:: geometry () const

{

// d-ptr只会 被库代码访问

return d_ptr - > geometry ;

}

接下来,是以Widget 为基类的子类示例。

label.h

class Label : public Widget

{

// ...

String text () ;

 

private :

// 每个 类都维护自己独有的一个 d-指针

LabelPrivate * d_ptr ;

} ;

label.cpp

// 与WidgetPrivate 不同的是,作者决定直接在源文件中定义LabelPrivate

struct LabelPrivate

{

String text ;

} ;

 

Label:: Label () : d_ptr( new LabelPrivate)

{

}

 

String Label:: text ()

{

return d_ptr - > text ;

}

在以上的代码结构中,CuteApp永远不会直接访问到d-指针。并且,由于 d- 指针 只会被WidgetLib 访问,而 WidgetLib 又会在每次发布时被重新编译,因此,那两个Private 类就可以自由地改变其实现方式,而不会影响到CuteApp。

d-指针所带来的其它好处

好处不仅仅局限于二进制兼容性。d-指针还能带来其它好处:

  • •.隐藏实现细节 - 我们在发布WidgetLib 时,只需要发布头文件和二进制文件。那些.cpp文件可以保持闭源状态。

  • •.头文件中不会暴露出实现细节,因而可以作为应用编程接口参考文档来使用。

  • •.由于与实现细节相关的头文件被从头文件中移动到了实现(源代码)文件中,所以,编译起来会更快。

当然,相比之下,以上列出的好处确实只是轻微的好处。在Qt 中使用 d-指针的真正原因就是,保持二进制兼容性,并且,Qt 在一开始是闭源软件。

q-指针

到这个阶段,我们仅仅 是将d-指针作为一个C-风格的数据结构来看待。 而在实际项目中, 它会包含有私有方法 (辅助函数) 。例如, 在 LabelPrivate 中,可能会需要一个辅助函数 getLinkTargetFromPoint() , 它的作用是,在发生鼠标点击时,用来找到链接 的目标地址。 在狠多情况下,这些辅助方法都需要访问到公有类,也就是,访问到Label 或其基类Widget 中的函数。例如 ,某个辅助函数, setTextAndUpdateWidget() ,可能需要调用 Widget::update() ,后者 是一个公有方法,用来计划下次 重绘该Widget。因此 , WidgetPrivate 也会存储一个指向其公有类的指针,称为q-指针。 对上面的代码进行修改,以实现q-指针模式,结果如下:

widget.h

class WidgetPrivate;

 

class Widget

{

// ...

Rect geometry () const ;

// ...

private :

WidgetPrivate * d_ptr ;

} ;

widget_p.h

struct WidgetPrivate

{

// 构造函数 ,初始化 q-ptr

WidgetPrivate ( Widget * q ) : q_ptr ( q ) { }

Widget * q_ptr ; // q-ptr指向应用编程接口 类

Rect geometry ;

String stylesheet ;

} ;

widget.cpp

#include "widget_p.h"

// 创建私有数据。

// 传入'this'指针,以初始化q-ptr

Widget:: Widget () : d_ptr( new WidgetPrivate( this ))

{

}

 

Rect Widget:: geometry () const

{

// d-ptr只会 被库代码访问

return d_ptr - > geometry ;

}

接下来,是另一个基于Widget 的类。

label.h

class Label : public Widget

{

// ...

String text () const ;

 

private :

LabelPrivate * d_ptr ;

} ;

label.cpp

// 与WidgetPrivate 不同的是,作者决定直接在源文件中定义LabelPrivate

struct LabelPrivate

{

LabelPrivate ( Label * q ) : q_ptr ( q ) { }

Label * q_ptr ;

String text ;

} ;

 

Label:: Label () : d_ptr( new LabelPrivate( this ))

{

}

 

String Label:: text ()

{

return d_ptr - > text ;

}

继承d-指针,以达到优化目的

在以上代码中,每当创建单个Label 对象 ,就会导致在内存中为 LabelPrivate 和 WidgetPrivate 分配空间。如果 我们在Qt 中采用这种策略,那么,对于像 QListWidget 这样的类,情况会更糟 - 它在类继承层级关系中处于6 层的深度, 这将导致 6 次内存分配!

对于这种问题,解决办法是,让我们的 私有 类组成继承层级关系,并让实际被实例化的类向上逐层传递单个d-指针。

注意,在进行 d-指针的继承时,私有类的声明就必须放置在单独的文件中,例如widget_p.h。再也不能将它声明到widget.cpp 文件中了。

widget.h

class Widget

{

public :

Widget () ;

// ...

protected :

// 只有子类能够访问 以下部分

// 允许子类 以自己独有的实体Private类来进行初始化

Widget ( WidgetPrivate & d ) ;

WidgetPrivate * d_ptr ;

} ;

widget_p.h

struct WidgetPrivate

{

WidgetPrivate ( Widget * q ) : q_ptr ( q ) { } // 构造函数 ,初始化 q-ptr

Widget * q_ptr ; // q-ptr ,指向应用编程接口类

Rect geometry ;

String stylesheet ;

} ;

widget.cpp

Widget:: Widget () : d_ptr( new WidgetPrivate( this ))

{

}

 

Widget:: Widget (WidgetPrivate &d) : d_ptr( &d)

{

}

label.h

class Label : public Widget

{

public :

Label () ;

// ...

protected :

Label ( LabelPrivate & d ) ; // 允许Label 的子类来传入它们自己独有的 Private 类

// 注意 , Label 并不具有 d_ptr !它直接使用Widget的d_ptr。

} ;

label.cpp

#include "widget_p.h"

 

class LabelPrivate : public WidgetPrivate

{

public :

String text ;

} ;

 

Label:: Label ()

: Widget ( * new LabelPrivate ) // 使用 我们自己独有的Private类来初始化 d-指针

{

}

 

Label:: Label (LabelPrivate &d) : Widget(d)

{

}

有没有感受到其中的美妙之处?如今, 当我们创建一个 Label 对象时,它会创建一个 LabelPrivate ( 它是的 WidgetPrivate 子类 ) 。 它向Widget 的受保护构造函数中传入了实体 d- 指针 ! 这样,当某个 Label 对象被创建时,只会发生一次内存分配。 Label 同样有一个 受 保护的构造函数, 可 由其子类传入独有的私有类 。

Qt中的d-指针

在Qt 中,实际上 , 公有类都使用了d-指针技术。只有少数情况 下没有使用这种技术,对于那些特例 ,都是事先明确知道 了,日后 再也不会向那个 类中加入 新的成员变量了。例如 ,对于像 QPoint 和 QRect 这样的类来说, 可以预期,不会再向它们添加任何新的成员了 。因此,数据成员 都是直接储存 在那些类本身, 而不是使用d-指针。

注意 ,在Qt 中,所有Private 对象 的基类都是 QObjectPrivate 。

Q_D和Q_Q

我们在之前的步骤中进行的优化,导致了一个副作用,那就是, q-ptr 和 d-ptr 的类型分别是 Widget 和 WidgetPrivate 。 这就意味着,以下代码无法正常工作。

void Label:: setText ( const String &text)

{

// 无法正常工作 !因为,尽管 d_ptr指向 一个LabelPrivate 对象,但它 的类型是 WidgetPrivate

d_ptr - > text = text ;

}

因此,在从子类中访问时d-指针,我们需要将它静态转换(static_cast)成适当的类型。

void Label:: setText ( const String &text)

{

LabelPrivate * d = static_cast < LabelPrivate * > ( d_ptr ) ; // 转换 成我们自己的私有类型

d - > text = text ;

}

如妳所见,如果代码中到处都是static_cast 的话,狠丑。为了解决这个问题,在src/corelib/global/qglobal.h 中定义了两个宏,使得我们可以写出直观的代码:

global.h

#define Q_D(Class) Class##Private * const d = d_func()

#define Q_Q(Class) Class * const q = q_func()

label.cpp

// 利用Q_D,妳就可以从Label中使用 LabelPrivate 的成员了

void Label:: setText ( const String &text)

{

Q_D ( Label ) ;

d - > text = text ;

}

 

// 利用Q_Q,妳就可以从LabelPrivate中使用 Label的成员了

void LabelPrivate:: someHelperFunction ()

{

Q_Q ( Label ) ;

q - > selectAll () ;

}

Q_DECLARE_PRIVATE和Q_DECLARE_PUBLIC

对于Qt 类,还 可以 在公有类中使用 Q_DECLARE_PRIVATE 宏。 这个宏的定义如下:

qglobal.h

#define Q_DECLARE_PRIVATE(Class)\

inline Class##Private* d_func() {\

return reinterpret_cast<Class##Private*>(qGetPtrHelper(d_ptr));\

}\

inline const Class##Private d_func() const {\

return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr));\

}\

friend class Class##Private;

这个宏,可以如下使用:

qlabel.h

class QLabel

{

private :

Q_DECLARE_PRIVATE ( QLabel ) ;

} ;

这种做法,其原理是,让 QLabel 提供一个函数 d_func() , 以便于让外界以某种手段来访问到它的私有内部类。 这个方法本身是私有的 (因为 这个宏位于qlabel.h 的私有区域 ) 。 但是 , d_func() 可被 QLabel 的 友元对象 (C++ 友元对象 )调用。 这种方法,其主要目的在于, 让某些无法通过公有接口访问 QLabel 中某些信息的Qt 类能够访问到对应的信息。 举个特殊例子, QLabel 可能 会记录下来,用户 在某个链接上点击了多少次 。但是 ,并没有公有的接口使得其它类能够访问到这个信息。 QStatistics 是一个工具 类,它需要这个信息。那么,某个 Qt开发 者会将 QStatistics 添加为 QLabel 的友元类, 这样, QStatistics 就可以调用 label->d_func()->linkClickCount 。

d_func 还有一个优点,它能够确保常量正确性(const-correctness): 在 MyClass 的常量 (const) 成员函数中,妳需要使用 Q_D(const MyClass) ,因而 , 就只能调用MyClassPrivate 中的常量(const)成员函数。如果使用 裸露 的 d_ptr ,那么,妳将会也能够调用非常量(non-const)函数。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值