QT之D指针

什么是D指针

如果你已经看过到Qt源码,你会发现它经常使用Q_DQ_Q`宏。本文介绍了这些宏的用途。

Q_DQ_Q宏是一个设计模式的一部分被称为d-指针(也称为*不透明的指针*,其中一个库的实现细节可以从它的用户,并转移到执行被隐藏),另外也保护了库的二进制兼容性。

在设计类似Qt的库时,希望即使在将Qt库升级/替换为另一个版本之后,也可以继续运行动态链接到Qt的应用程序而无需重新编译。例如,如果您的应用程序CuteApp基于Qt 4.5,则应该能够将Qt库(在该应用程序附带的Windows上,在Linux上通常自动来自程序包管理器!)从版本4.5升级到Qt 4.6,而您的CuteApp使用Qt 4.5构建的窗口仍应能够运行。

什么破坏了二进制兼容性?

那么,库中的更改何时需要重新编译应用程序?让我们举一个简单的例子:

class Widget 
 { 
 // ... 
 private:
    Rect m_geometry; 
 }; 
 
 class Labelpublic Widget 
 { 
 public// ...
    string text()const 
    { 
        return m_text; 
    } 
 
 private:
     string m_text; 
 };

在这里,我们有一个小部件,其中包含几何作为成员变量。我们编译Widget并将其作为WidgetLib 1.0交付

对于WidgetLib 1.1,有人想出了一个聪明的主意来添加对样式表的支持。不费吹灰之力,我们只需添加新方法并添加新的数据成员

class Widget 
 { 
    // ... 
 private:
     Rect m_geometry; 
     string m_stylesheet; // WidgetLib 1.1中的新增功能
 }; 
 
 class Labelpublic Widget 
 { 
 public// ...
     string text()const 
     { 
         return m_text; 
     } 
 
 private:
     string m_text; 
 };
 

我们提供了带有上述更改的WidgetLib 1.1,只是发现在WidgetLib 1.0中编译并运行良好的CuteApp崩溃崩溃了!

为什么会崩溃?

原因是通过添加新的数据成员,我们最终更改了Widget和Label对象的大小。为什么这么重要?当您的C ++编译器生成代码时,它使用偏移量来访问对象内的数据(一般项目,在头文件有过改动,都需要应用程序重新编译,免除一些奇怪的问题出现)。

这是上述POD对象在内存中的外观的简化版本。

WidgetLib 1.0中的标签对象布局WidgetLib 1.1中的标签对象布局
m_geometry <offset 0>m_geometry <offset 0>
m_stylesheet <offset 1>
m_text
m_text <offset 2>

在WidgetLib 1.0中,Label的文本成员位于(逻辑)偏移量1。由应用程序中的编译器为Label :: text()方法生成的代码转换为访问应用程序中Label对象的偏移量1。在WidgetLib 1.1中,Label的文本成员已移至(逻辑)偏移量2!由于尚未重新编译应用程序,因此它继续认为文本在偏移量1处,并最终访问了 样式表变量!

我可以肯定地说,有些人想知道为什么Label :: text()的偏移量计算代码最终出现在CuteApp二进制文件而不是WidgetLib二进制文件中。答案是在头文件中定义了Label :: text()的代码,编译器最终对其进行了*内联*。

那么,如果没有内联Label :: text(),情况会改变吗?说,Label :: text()已移至源文件?好吧,不。C ++编译器依赖于在编译时和运行时相同的对象大小。例如,堆栈缠绕/展开-如果在堆栈上创建了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_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 
{ 
    //仅在库代码
    return d_ptr->geometry;
}

接下来,有一个基于Widget的子类的示例。

Label.h

class Labelpublic Widget 
{ 
    // ...
    string text(); 

private//每个类维护自己的d指针
    LabelPrivate * d_ptr; 
};

Label.cpp

// Unlike WidgetPrivate, the author decided LabelPrivate
// to be defined in the source file itself
struct LabelPrivate
{
    String text;
};

Label::Label() : d_ptr(new LabelPrivate)
{
}

String Label::text()
{
    return d_ptr->text;
}

通过上述结构,CuteApp永远不会直接访问d指针。并且由于仅在WidgetLib中访问了d指针,并且每个版本都对WidgetLib进行了重新编译,因此Private类可以自由更改,而不会影响CuteApp。

d指针的其他好处

二进制兼容性并不是全部。d指针还有其他好处:

  • 隐藏实现细节-我们可以将WidgetLib与头文件和二进制文件一起提供。.cpp文件可以是封闭源。
  • 头文件中没有实现细节,可以用作API参考。
  • 由于实现所需的头文件已从头文件移至实现(源)文件,因此编译速度要快得多。

确实,上述好处显得微不足道。在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
{
    // Constructor that initializes the q-ptr
    WidgetPrivate(Widget *q) : q_ptr(q) { }
    Widget *q_ptr; // q-ptr points to the API class
    Rect geometry;
    String stylesheet;
};

widget.cpp

#include "widget_p.h"
// Create private data.
// Pass the 'this' pointer to initialize the q-ptr
Widget::Widget() : d_ptr(new WidgetPrivate(this))
{
}

Rect Widget::geometry() const
{
    // the d-ptr is only accessed in the library code
    return d_ptr->geometry;
}

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

label.h

class Label : public Widget
{
    // ...
    String text() const;

private:
    LabelPrivate *d_ptr;
};

label.cpp

// Unlike WidgetPrivate, the author decided LabelPrivate
// to be defined in the source file itself
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将导致为LabelPrivateWidgetPrivate分配内存。如果我们将这种策略用于Qt,则对于类似QListWidget的类,情况将变得更加糟糕-它在类继承层次结构中位于6级,这将导致最多6个内存分配(每一个继承都会分配一个d指针的内存分配)!

这可以通过为我们的私有类建立继承层次结构,并使实例化的类一直沿d指针传递来解决。

请注意,在继承d指针时,私有类的声明必须在单独的文件中,例如widget_p.h。不再可以在widget.cpp文件中声明它。

widget.h

class Widget
{
public:
    Widget();
    // ...
protected:
    // only subclasses may access the below
    // allow subclasses to initialize with their own concrete Private
    Widget(WidgetPrivate &d);
    WidgetPrivate *d_ptr;
};

widget_p.h

struct WidgetPrivate
{
    WidgetPrivate(Widget *q) : q_ptr(q) { } // constructor that initializes the q-ptr
    Widget *q_ptr; // q-ptr that points to the API class
    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); // allow Label subclasses to pass on their Private
    // notice how Label does not have a d_ptr! It just uses Widget's d_ptr.
};

label.cpp

#include "widget_p.h"

class LabelPrivate : public WidgetPrivate
{
public:
    String text;
};

Label::Label()
 : Widget(*new LabelPrivate) // initialize the d-pointer with our own Private
{
}

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

你看到这么使用的优点了吗?现在,当我们创建Label对象时,它将创建LabelPrivate(子类WidgetPrivate)。它将具体的d指针传递给Widget的受保护构造函数!现在,创建Label对象时,只有一个内存分配。Label还具有受保护的构造函数,其子类可以使用其提供自己的私有类。

Qt中的d指针

在Qt中,几乎每个公共类都使用d指针方法。唯一不使用它的情况是,当事先知道该类永远不会添加任何成员变量时。例如,对于QPointQRect之类的类,不希望添加任何新成员,因此数据成员直接存储在类本身中,而不是使用d指针。

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

Q_D和Q_Q

我们在上一步中所做的优化的副作用是q-ptr和d-ptr的类型为WidgetWidgetPrivate。这意味着以下操作无效。

void Label :: setText(const String&text)
{ 
   //不起作用!因为d_ptr的类型为WidgetPrivate,即使它
   //指向LabelPrivate对象
   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

// With Q_D you can use the members of LabelPrivate from Label
void Label::setText(const String &text)
{
    Q_D(Label);
    d->text = text;
}

// With Q_Q you can use the members of Label from LabelPrivate
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 ++朋友)调用。这对于Qt类访问信息非常有用,因为Qt类无法使用公共api访问某些QLabel信息。举一个奇怪的例子,QLabel可能会跟踪用户单击链接的次数。但是,没有公共API可以访问此信息。QStatistics是需要此信息的类。Qt开发人员将添加`````````QStatistics作为QLabel的朋友,然后QStatistics可以执行label-> d_func()-> linkClickCount`。

d_func还具有的优点是执行常量正确性:在MyClass的一个const成员函数,你需要一个Q_D(常量MyClass的),因此你只能调用const成员函数中MyClassPrivate。使用裸露的d_ptr,您还可以调用非const函数。

Qt使用d指针和q指针来实现封装和隐藏内部实现细节,以及提供更好的代码可读性和可维护性。同时,Qt还使用命名空间来组织和管理类和函数。 下面是一个示例代码,演示了Qt中如何使用d指针、q指针和命名空间: ```cpp // counter.h #ifndef COUNTER_H #define COUNTER_H #include <QObject> namespace MyNamespace { class CounterPrivate; // 前向声明 class Counter : public QObject { Q_OBJECT public: explicit Counter(QObject *parent = nullptr); ~Counter(); void increment(); void decrement(); int value() const; private: CounterPrivate *d_ptr; // d指针 }; } #endif // COUNTER_H // counter.cpp #include "counter.h" namespace MyNamespace { class CounterPrivate { public: int count; }; Counter::Counter(QObject *parent) : QObject(parent), d_ptr(new CounterPrivate) { d_ptr->count = 0; } Counter::~Counter() { delete d_ptr; } void Counter::increment() { ++d_ptr->count; } void Counter::decrement() { --d_ptr->count; } int Counter::value() const { return d_ptr->count; } } // main.cpp #include <QGuiApplication> #include <QQmlApplicationEngine> #include "counter.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); MyNamespace::Counter counter; QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("Counter", &counter); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); return app.exec(); } ``` 在这个示例中,我们定义了一个名为Counter的类,它位于MyNamespace命名空间中。Counter类使用了d指针来隐藏内部实现细节,并提供了increment、decrement和value等公共接口来操作计数器的值。在main.cpp中,我们创建了Counter的实例,并将其作为上下文属性传递给QML引擎。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值