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);
}
运行结果: