一 为什么要使用q/d指针
在创建QObject时我们都会遇到下面这样的属性在QObject类中,在QObject中使用智能指针创建了一个QObjectData类的指针对象,而在QObject类中,我们并未发现成员变量的属性定义。而是定义在了QObjectData类中,显而易见根据类名,这个类中存放了QObject的成员属性。
QScopedPointer<QObjectData> d_ptr;
//QObjectData的声明
class Q_CORE_EXPORT QObjectData {
public:
virtual ~QObjectData() = 0;
QObjecclass Q_CORE_EXPORT QObjectData {
public:
virtual ~QObjectData() = 0; // 纯虚析构函数,表示这是一个抽象基类
QObject *q_ptr; // 对象的 this 指针
QObject *parent; // 父对象的指针
QObjectList children; // 子对象列表
uint isWidget : 1; // 标记是否是窗口部件
uint blockSig : 1; // 标记是否阻止信号
uint wasDeleted : 1; // 标记对象是否已删除
uint isDeletingChildren : 1; // 标记是否正在删除子对象
uint sendChildEvents : 1; // 标记是否发送子对象事件
uint receiveChildEvents : 1; // 标记是否接收子对象事件
uint isWindow : 1; // 用于 QWindow,标记是否是窗口
uint deleteLaterCalled : 1; // 标记是否已调用 deleteLater 函数
uint unused : 24; // 未使用的位,预留的字节数
int postedEvents; // 发送的事件数量
QDynamicMetaObjectData *metaObject; // 动态元对象数据
QMetaObject *dynamicMetaObject() const; // 获取动态元对象
};
qt为什么要使用这种方式封装类呢? 原因是因为为了让动态库实现二进制兼容
1.1 二进制兼容的动态库
二进制兼容动态库:在旧版本的动态库上能运行的程序能够在不重新编译的情况下成功的在新版本的动态库下运行就说明该动态库是二进制兼容的动态库,在老版本动态库上运行新的应用程序,在不经过编译的情况下,能够在新版本动态库下运行;
1.2 源代码兼容的动态库
在需要编译的情况下,不需要修改源码,我们就说动态库是兼容源代码的
1.3 解决方案:
要想dll,so库能够达到二进制兼容,对于类中对象/结构/数据都应保持不变,如果对类中的对象或者任何数据进行改动,就会影响数据模型,从而导致数据在数据模型中发生变化,程序使用改动后的新版本动态库编译后将导致程序崩溃,此时就需要重新编译源码。在大型项目开发中会极为不便,并且繁琐。
应对方法:使动态库增加数据后对动态库的数据模型的大小不会产生影响
1.预先分配若干个保留空间,后续添加数据使用保留空间(使用位域定义: int n :20 即该变量预先占用几个bit空间) --浪费空间 比如qt中的QObject就使用了这种方式,但这种方式首先是浪费空间,其次是如果预留空间不足仍然是不能满足兼容。
2.将预分配保留空间的类型由常规变量改为"对象指针‘
二 q_ptr和d_ptr的使用
1.1 作用
隐藏接口的具体实现细节
提高程序编译速度
最大程度实现二进制兼容
1.2 Q_DECLARE_PRIVATE宏
#define Q_DECLARE_PRIVATE(Class) \
inline Class##Private* d_func() \
{ Q_CAST_IGNORE_ALIGN(return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr));) } \
inline const Class##Private* d_func() const \
{ Q_CAST_IGNORE_ALIGN(return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr));) } \
friend class Class##Private;
在Q_DECLARE_PRIVATE宏中 定义了一个函数d_func() 会返回 传入的类名拼装上Private的类,这个类其实就是用于存放数据的对象指针。以及一个const属性的ClassPrivate类,这两个一个用于设置改变其中的值,一个用于获取成员的值,个人认为其实就相当于set get方法。get方法就需要加上const属性返回。
以及一个友元类class##Private。(友元类 : 允许class##Private这个类访问class的私有成员属性,切记并不是class可以访问class##Private这个类的私有属性)
1.3 Q_DECLARE_PUBLIC宏
#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;
在Q_DECLARE_PUBLIC宏中与Q_DECLARE_PRIVATE大同小异不再介绍
1.4 Q_D宏和Q_Q宏
#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()
这两个宏一方面是为了使用方便,另一方面为d_func()和q_func()增加了const属性来修饰指针,防止误操作给指针重定向。
三 使用q/d指针封装
示例:在QLabel这个类中,我们如果想要为QLabel直接传入QColor来设置颜色是需要经过Palette调色板来进行上色,然后给QLabel设置QPattle才可以,这里我们使用q/d指针为QLabel类封装一个setColor方法.
3.1 私有对象指针
改类其实是数据类,存放数据,这样就不会使得原有的类在增加数据后改变之前的成员变量在内存中的内存模型。
其实是封装数据
class CLabelPrivate
{
Q_DECLARE_PUBLIC(CLabel)
public:
CLabelPrivate(CLabel *parent) : q_ptr(parent){}
private:
Qt::GlobalColor m_color;
CLabel *q_ptr;
};
3.2 类方法封装
//.h
class CLabel : public QLabel
{
Q_OBJECT
public:
CLabel(QWidget* parent = nullptr);
void setColor(Qt::GlobalColor color);
Qt::GlobalColor getColor() const;
virtual ~CLabel();
protected:
QScopedPointer<CLabelPrivate> d_ptr;
private:
Q_DECLARE_PRIVATE(CLabel)
Q_DISABLE_COPY(CLabel)
};
//CPP
CLabel::CLabel(QWidget *parent) : QLabel(parent) ,d_ptr(new CLabelPrivate(this))
{
}
void CLabel::setColor(Qt::GlobalColor color)
{
Q_D(CLabel);
d->m_color = color;
QPalette _palette;
_palette.setColor(QPalette::WindowText,color);
this->setPalette(_palette);
}
Qt::GlobalColor CLabel::getColor() const
{
Q_D(const CLabel);
return d->m_color;
}
CLabel::~CLabel()
{
}
如果程序是用于发布成动态库,即使在C++中我们也应该运用这样的封装思想。
调用:
CLabel* lb = new CLabel(this);
lb->setColor(Qt::red);
lb->setText("qd指针");
lb->move(10,10);
qDebug()<<lb->getColor();
其实这就是两个双向关联的类,并利用友元函数让他们相互之间都能够访问彼此的成员属性和方法.