Qt内部的d指针和q指针:Q_DECLARE_PRIVATE是干嘛的

Q_DECLARE_PRIVATE是干嘛的

        Q_DECLARE_PRIVATE就是实现Qt内部的d指针和q指针的功能宏命令。如果想了解具体的使用与内部信息我们就得了解Qt内部的d指针和q指针

Qt内部的d指针和q指针

        Qt内部的d指针和q指针是保证模块间的二进制兼容。也就是如果我们使用插件式开发项目,调用静态/动态库时,保证库的开发版本变化,仍然可以正常调用工作的技术,我们称之为二进制兼容。

二进制兼容性

        二进制兼容简单说就是如果自己的项目调用了开发好的库,二进制兼容可以保证在升级了库文件后(比如XX.lib从1.0升级到2.0),自己的项目可以不用重新编译就能够兼容修改后的XX.lib 2.0。 二进制指的是编译生成的库文件,一旦编译好的库文件,其内部内存布局是固定的,如果我们想调用库文件里的对象、或是变量,其实就是利用偏移寻址找到我们想调用的东西。当我们第一次编译项目时(整个项目完全编译),项目是知道我们调用的XX.lib的内存布局的。如果,我们不编译我们的主项目程序,只是升级了xx.lib库版本,在xx.lib中我们添加了变量或其他数据项(静态数据除外),很显然,xx.lib的内存布局发生了变化。这时我们的项目主体程序(调用者)还不知道xx.lib内存布局发生了变化,一旦项目主体程序(调用者)调用xx.lib中的数据,想当然的就会发生错误导致崩溃。

这是摘抄至 KDE Community Wiki (这个博客详细介绍了二进制兼容性,有兴趣的朋友可以详细研究一下)的一段话:

二进制兼容:如果动态链接到库的旧版本的程序继续使用库的新版本运行,而不需要重新编译,则库是二进制兼容的。如

源代码兼容:果程序需要重新编译才能使用新版本的库运行,但不需要任何进一步修改,则该库是源代码兼容的。

        二进制兼容性节省了很多麻烦。它使得为某个平台分发软件变得更加容易。如果不确保版本之间的二进制兼容性,人们将被迫提供静态链接的二进制文件。

静态二进制文件有很大的缺点,因为它们:

 
  • 浪费资源(尤其是内存)
  • 不允许程序从库中的错误修复或扩展中获益

        在KDE项目中,我们将在核心库(KDELIB、KDEPIMLIB)的主要版本的生命周期内提供二进制兼容性。

        这里是会对二进制兼容破坏的操作(这里仅仅列出了数据成员的破坏操作,对函数的改动等都会破坏二进制的兼容性,这里我们能够理解就可以了。):

  • 将新的数据成员添加到现有的类
  • 改变一个类中的非静态数据成员的顺序
  • 修改成员的类型,修改符号例外 (例如:signed/unsigned 改来改去,不影响字节长度)
  • 从现有的类中删除现有的非静态数据成员

我在国内的很多博客里看到:“调整私有成员变量的顺序会造成二进制不兼容”,“私有成员是二进制兼容的大敌”。对于这两句话我没有找到有用的证据证明这一点。对二进制兼容的破坏是多样性的。没有任何证据显示私有成员的特别性。这个话题有待论证?

 理解Qt内部的d指针和q指针的原理

        首先我们关心的是二进制兼容性问题,并不去理会Qt内部的d指针和q指针,因为它只是QT解决兼容性问题的解决方案。在C++中解决二进制兼容性的方案在不同的框架中都有不同的实现方式,但是原理是一样的。

        在了解指针的情况下,很当然的我们会想到利用指针去解决二进制兼容问题。因为我们只用在插件(库文件xx.lib)中声明一个指向外部的指针。无论我们的对指针指向的数据怎样变化,存储指针的内存顺序和空间是不会变化的。指针指向外部的结构体其实就在插件库中,当我们更新了插件库,那指针所指向的外部也属于插件库的一部分,当然也更新了。这样就很好的避开了二进制兼容的陷阱。

        我们利用上一段的推理去实现这种方案。假设我们有一个播放器,我们需要实现播放列表。这个播放列表我们希望它以插件(动态链接库)的方式调用。

QMediaPlaylist.h

#ifndef QMEDIAPLAYLIST_H
#define QMEDIAPLAYLIST_H

#include <QWidget>
#include "QMediaPlaylistPrivate.h"

class QMediaPlaylistPrivate;

class QMediaPlaylist : public QWidget
{
    Q_OBJECT

public:
    QMediaPlaylist(QWidget *parent = nullptr):d_ptr(new QMediaPlaylistPrivate){ d_ptr->q_ptr = this; }
    ~QMediaPlaylist(){ if(d_ptr);

    //这里很多大牛举的例子采用了内联方式声明函数,这样更能明显的显露二进制兼容的陷阱
    void setNumber(int num) { d_ptr->number = num; }
    int getNumber() { return d_ptr->number; }

protected:
    QMediaPlaylistPrivate* d_ptr = nullptr;

};
#endif // QMEDIAPLAYLIST_H

QMediaPlaylistPrivate.h代码:

#ifndef QMEDIAPLAYLISTPRIVATE_H
#define QMEDIAPLAYLISTPRIVATE_H

//有些朋友可能对这句有些疑惑,可以查询指针与前置声明
class QMediaPlaylist;

class QMediaPlaylistPrivate
{
  public:
    QMediaPlaylistPrivate(){}
    ~QMediaPlaylistPrivate(){}

    int number;
    QMediaPlaylist* q_ptr;
};

#endif // QMEDIAPLAYLISTPRIVATE_H

main.h代码(main里的UI代码不是必须的,我只是为了更好的实现。):

#include "QMediaPlaylist.h"

#include <QApplication>
#include <QLabel>
#include <QVBoxLayout>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QMediaPlaylist w;

    w.setNumber(10);

    QLabel *label = new QLabel(&w);
    label->setText(QString::number(w.getNumber()));

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(label);
    w.setLayout(layout);
    w.setGeometry(100,100,260,100);


    w.show();
    return a.exec();
}

  在这个样例中QMediaPlaylistPrivate作为QMediaPlaylist的类,我们在改动QMediaPlaylist的时候并不会破坏QMediaPlaylist的二进制兼容性。因为我们暴漏给项目的主体程序只是QMediaPlaylist的接口,而操纵QMediaPlaylistPrivate中的数据是QMediaPlaylist利用一直没有变化的指针操作的。QMediaPlaylist相对于项目主体程序是永远没有改变内存布局的,因为它隐藏了细节。

        上面的样例看起来已经完全解决了二进制兼容性的问题。而QT给的d指针和q指针的解决方案看起来要比我们多做了更多的事情。原因在于QT考虑的更周到,考虑了系统的优化。我在鬼谷子com的博客里发现在多层继承中,最顶端的父类中QMediaPlaylistPrivate对象构造函数会多次重建,这样会造成资源的浪费。我们清楚,子类的初始化会首先调用基类的构造函数,这是一次计数。我们基类和子类都会包含D指针,这样就会再次触发基类的构造函数,这又是N次计数。这里假设我们有多次继承发生。鬼谷子com的一篇文章详细介绍了优化思路。

通过上面的理解总结D指针的好处:

  • 保证代码的二进制兼容性;
  • 隐藏实现细节;
  • 提高编译速度;
  • 优化占用资源。

再谈Qt内部的d指针和q指针

        我们在Qt中看到的两个宏Q_D和Q_Q这两个宏分别是取得d指针和q指针的,d指针指向封装的私有类,q指针指向公共的类。Q_D和Q_Q替我们封装了我们之前的思路(但,不一定会是同样的技术)。所有的细节操作由这两个宏和Objedt基类替我们做了。

理解D指针

首先看一下 Q_D 的Q_DECLARE_PRIVATE()宏定义:

#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;  

 代码中的# 和 ## 的功能分别是:

  • # 是把宏参数变成一个字符串
  • ## 是将2个宏参数连接在一起

 上面的代码替换之后就成了:

 ClassPrivate * const d = d_func()

//上面的代码就是由这个宏生成的
#define Q_D(Class) Class##Private * const d = d_func()

  d_func() 函数实际上就是返回了一个 ClassPrivate 类型的指针,在我们上面的样例中就是QMediaPlaylistPrivate指针。在样例中我们可以推导出这样的结果:

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

//在我们的实例中就相当于
#define Q_D(QMediaPlaylist) QMediaPlaylistPrivate * const d = d_func()

        QMediaPlaylist 通过 Q_DECLARE_PRIVATE(QMediaPlaylist) 定义了一个 d_func() 的函数, 可以通过 d_func() 返回 QMediaPlaylistPrivat 对象的指针。

现在我们使用QT的Q_D重构之前的 QMediaPlaylist.h 代码:

#ifndef QMEDIAPLAYLIST_H
#define QMEDIAPLAYLIST_H

#include <QWidget>
#include "QMediaPlaylistPrivate.h"

class QMediaPlaylistPrivate;

class QMediaPlaylist : public QWidget
{
    Q_OBJECT

public:
    QMediaPlaylist(QWidget *parent = nullptr);
    ~QMediaPlaylist()=default;

    void setNumber(int num);
    int getNumber();

protected:
    QMediaPlaylistPrivate *d_ptr;
    Q_DECLARE_PRIVATE(QMediaPlaylist);

};
#endif // QMEDIAPLAYLIST_H

QMediaPlaylist.cpp代码:

#include "QMediaPlaylist.h"

QMediaPlaylist::QMediaPlaylist(QWidget *parent):
    QWidget(parent),
    d_ptr(new QMediaPlaylistPrivate)
{
    Q_D(QMediaPlaylist);
    d->q_ptr = this;
}

void QMediaPlaylist::setNumber(int num)
{
    d_ptr->number = num;

    //返回的指针是智能指针:QScopedPointer<QObjectData>
    Q_D(QMediaPlaylist);
    d->number = num+1;

    qDebug()<<d;      //0x1f8131c0380
    qDebug()<<d_ptr;  //0x1f8131c0380
}

int QMediaPlaylist::getNumber()
{
    return d_ptr->number;
}

至于Q_D的内部实现,可以研究一下实现源码。

理解Q指针

首先看一下Q_Q 的Q_DECLARE_PRIVATE()宏定义:

#define Q_DECLARE_PUBLIC(Class) \
 inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
 inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
 friend class Class;

我们通过QMediaPlaylistPrivate中 q_func() 获得 QMediaPlaylist 的指针 q_ptr。而q_ptr在QMediaPlaylist构造时已经初始化。所以我们在QMediaPlaylistPrivate声明得函数内就可以调用QMediaPlaylist资源了。

下面是完整得实验代码:

main.cpp

#include "QMediaPlaylist.h"

#include <QApplication>
#include <QLabel>
#include <QVBoxLayout>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QMediaPlaylist w;

    w.setNumber(10);

    QLabel *label = new QLabel(&w);
    label->setText(QString::number(w.getNumber()));

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(label);
    w.setLayout(layout);
    w.setGeometry(100,100,260,100);


    w.show();
    return a.exec();
}
QMediaPlaylist.h
#include "QMediaPlaylist.h"

#include <QApplication>
#include <QLabel>
#include <QVBoxLayout>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QMediaPlaylist w;

    w.setNumber(10);

    QLabel *label = new QLabel(&w);
    label->setText(QString::number(w.getNumber()));

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(label);
    w.setLayout(layout);
    w.setGeometry(100,100,260,100);


    w.show();
    return a.exec();
}
QMediaPlaylist.cpp
#include "QMediaPlaylist.h"
#include "QMediaPlaylistPrivate.h"

QMediaPlaylist::QMediaPlaylist(QWidget *parent):
    QWidget(parent),
    d_ptr(new QMediaPlaylistPrivate)
{
    Q_D(QMediaPlaylist);
    d->q_ptr = this;
}

void QMediaPlaylist::setNumber(int num)
{
    d_ptr->number = num;

    //返回的指针是智能指针:QScopedPointer
    Q_D(QMediaPlaylist);
    d->number = num+1;

    qDebug()<<d;
    qDebug()<<d_ptr;
}

int QMediaPlaylist::getNumber()
{
    Q_D(QMediaPlaylist);
    d->setNumber();
    return d_ptr->number;
}
QMediaPlaylistPrivate.h
#include "QMediaPlaylist.h"
#include "QMediaPlaylistPrivate.h"

QMediaPlaylist::QMediaPlaylist(QWidget *parent):
    QWidget(parent),
    d_ptr(new QMediaPlaylistPrivate)
{
    Q_D(QMediaPlaylist);
    d->q_ptr = this;
}

void QMediaPlaylist::setNumber(int num)
{
    d_ptr->number = num;

    //返回的指针是智能指针:QScopedPointer
    Q_D(QMediaPlaylist);
    d->number = num+1;

    qDebug()<<d;
    qDebug()<<d_ptr;
}

int QMediaPlaylist::getNumber()
{
    Q_D(QMediaPlaylist);
    d->setNumber();
    return d_ptr->number;
}
QMediaPlaylistPrivate.cpp
#include "QMediaPlaylistPrivate.h"


QMediaPlaylistPrivate::QMediaPlaylistPrivate()
{
    this->number=0;
}


void QMediaPlaylistPrivate::setNumber()
{
    Q_Q(QMediaPlaylist);
    q->setNumber(100);
}

运行结果:

 

  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

开软古剑楠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值