Qt-D指针与Q指针的设计哲学

文章目录

  • 前言
    • PIMLP与二进制兼容性
    • D指针
    • Q指针
    • 优化d指针继承
    • Q_D和Q_Q

前言

在探索Qt源码的过程中会看到类的成员有一个d指针,d指针类型是一个private的类,这种设计模式称为PIMPL(pointer to implementation),本文根据Qt官方文章介绍d指针与q指针,理解其中的设计哲学。

PIMLP与二进制兼容性

PIMPL(pointer to implementation)或称Opaque Pointer指的是将一个类的功能实现通过另一个实现类的指针隐藏起来,例如头文件有如下声明:

class Widget {public:Widget();...
private:WidgetImpl* p;
}

在cpp中实现WidgetImpl类

class WidgetImpl {// implementation
}

这种设计模式通常使用在库的实现当中,因为它有两个好处:

  1. 隐藏类实现。包括一些没必要暴露给用户的内部函数声明,以及需要的成员变量等实现细节。
  2. 库的二进制兼容。当改变实现类的数据成员时,不影响库的二进制兼容性,原本使用库的主程序不需要重新编译,只需要把库文件(动态库)进行替换即可。

让我们看一个二进制不兼容的例子,假设有如下实现,并且将其编译成WidgetLib1.0动态库:

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

当我们有一天希望升级Widget类的功能,需要新增一个数据成员,如下:

 class Widget{// ...private:Rect m_geometry;String m_stylesheet; // NEW in WidgetLib 1.1};class Label : public Widget{public:// ...String text() const{return m_text;}private:String m_text;};

此时编译出WidgetLib1.1库后,替换1.0库,这时运行主程序会发生崩溃。原因在于我们新增了一个数据成员,从而改变了类Widget的大小,当编译器在编译生成底层代码时,它会使用到数据成员的偏移量从而访问某个对象的某一个数据成员,下面是WidgetLib1.0和WidgetLib1.1简化后label对象的内存分布对比。

在这里插入图片描述
在WidgetLib1.0中,m_text在label对象偏移量为1的位置,而在WidgetLib1.1中,m_text偏移量为2。对于主程序而言,在编译使用1.0版本的主程序时,主程序中调用text()接口的代码会被翻译成访问label对象偏移量为1的位置的数据,而在升级到WidgetLib1.1后,由于主程序没有重新编译,只是替换了库,库中的m_text偏移量变为了2,但是主程序由于没有重新编译,因此它访问的仍然是偏移量为1的位置,但是此时访问到的实际上是m_stylesheet的变量。
这里text()的代码实现的翻译在主程序中而不是在lib中,是因为其实现是在头文件中写的,那么如果不是写在头文件中结果有变化吗?答案是没有,因为编译器依赖于对象的大小生成代码,并且要求编译时和运行时的对象大小是一致的,如果我们主程序中声明了一个在栈上的label对象,编译器在编译时(主程序+1.0库)认为对象的大小是2,而升级库到1.1后,主程序运行时的实际大小为3,那么在创建对象的时候就会把栈中的数据覆盖掉从而破坏栈。

至此我们得出一个结论,如果希望程序在库升级后能继续使用,我们就不能改变类的大小

D指针

解决方法就是让导出的类拥有一个指针,这个指针指向了所有内部的数据,当内部数据的成员增减时,由于这个指针只在库中用到,因此只会影响到库,对主程序而言,类的大小一直都是一个内部数据指针的大小,因此不会对主程序产生影响,这个指针在Qt中称为D指针。

 /* Since d_ptr is a pointer and is never referenced in header file(it would cause a compile error) WidgetPrivate doesn't have to be included,but forward-declared instead.The definition of the class can be written in widget.cpp orin a separate file, say widget_p.h */class WidgetPrivate;class Widget{// ...Rect geometry() const;// ... private:WidgetPrivate *d_ptr;};

在widget_p.h中

/* widget_p.h (_p means private) */
struct WidgetPrivate
{Rect geometry;String stylesheet;
};

widget.cpp

// With this #include, we can access WidgetPrivate.
#include "widget_p.h"Widget::Widget() : d_ptr(new WidgetPrivate)
{// Creation of private data
}Rect Widget::geometry() const
{// The d-ptr is only accessed in the library codereturn d_ptr->geometry;
}
class Label : public Widget
{// ...String text();private:// Each class maintains its own d-pointerLabelPrivate *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;
}

由于d指针只在库中使用,而每次发布库都会重新编译,因此Private类可以随意更改而不会影响主程序。

这种实现方式有如下好处:

  1. 二进制兼容性
  2. 隐藏实现细节。只需一个头文件和一个库。
  3. 头文件没有实现细节相关的api,用户可以更清晰的看到能使用的api
  4. 编译更快。因为所有实现细节都从头文件都移到了实现类的cpp文件中

Q指针

有时在Private实现类中我们希望访问原有类的指针,调用它的一些函数,因此在实现类中通常会保存一个指针指向原有的类,这个指针我们称为Q指针。
widget.h

class WidgetPrivate;class Widget
{// ...Rect geometry() const;// ...
private:WidgetPrivate *d_ptr;
};

widget_p.h

struct WidgetPrivate
{// Constructor that initializes the q-ptrWidgetPrivate(Widget *q) : q_ptr(q) { }Widget *q_ptr; // q-ptr points to the API classRect 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 codereturn d_ptr->geometry;
}

‎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指针继承

注意到Widget和Label类都声明了一个各自类的Private类指针,且子类构造函数在实例化父类时使用的是默认无参的构造函数,因此,当我们实例化Label时,会先调用基类的构造函数,然后new WidgetPrivate,接着调用子类的构造函数,然后new LabelPrivate,对于某些深度继承的类,这种设计将会造成多次的内存申请,且有多个相互独立的Private类对象存在,子类的d_ptr还会覆盖父类的同名数据成员d_ptr。解决方法是让Private类也具有继承关系,将子类的指针沿着Private类继承链向上传递。注意这种方式要求Private父类要在单独的头文件中声明(而不是直接写在cpp文件中),否则无法被其他Private子类继承。改进后的设计如下:
widget.h

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

widget_p.h

struct WidgetPrivate
{WidgetPrivate(Widget *q) : q_ptr(q) { } // constructor that initializes the q-ptrWidget *q_ptr; // q-ptr that points to the API classRect 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对象,只会发生一次内存申请,即new LabelPrivate。

Q_D和Q_Q

在Label类方法中,当我们访问d_ptr时,访问的是基类声明的WidgetPrivate类型的指针,在Label类的方法中为了访问LabelPrivate的成员text,需要向下转换

void Label::setText(const String &text)
{LabelPrivate *d = static_cast<LabelPrivate*>(d_ptr); // cast to our private typed->text = text;
}

Qt定义了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()是把d_ptr进行类似static_cast<LabelPrivate*>的类型转换。于是代码简化为:

// 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();
}

d_func()通过Q_DECLARE_PRIVATE定义了两个版本,一个返回Private类指针,一个返回const Private类指针,此外Q_DECLARE_PRIVATE还声明ClassPrivate是Class的友元类,如下:

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

我们可以在公共类(导出类)声明一个Q_DECLARE_PRIVATE,从而快速声明返回const和非const Private类指针的d_func(),以及声明Private类是当前类的友元类,如下:

class QLabel
{
private:Q_DECLARE_PRIVATE(QLabel)
};

friend class Class##Private的作用是让Private类可以访问公共类的public/protected/private接口。
注意,当我们需要使用const版本的Private类指针时,使用如下写法:

Q_D(const Label);  // 自动调用const版本的d_func()

通常d_func()是在类内部使用,不过某些情况下,也可以通过声明friend class的方式使得其它类可以访问当前类内部的数据,这些数据通常无法从当前类的公共api得到,例如,在QLabel类中声明ClassA是友元类,ClassA对象访问QLabel内部数据的方法如下:

// ClassA声明为QLabel的friend class, ClassA方法中可以有如下调用
label->d_func()->linkClickCount;

参考:

  1. https://wiki.qt.io/D-Pointer

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/34552.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ctf web入门知识合集

文章目录 01做题思路02信息泄露及利用robots.txt.git文件泄露dirsearch ctfshow做题记录信息搜集web1web2web3web4web5web6web7web8SVN泄露与 Git泄露的区别web9web10 php的基础概念php的基础语法1. PHP 基本语法结构2. PHP 变量3.输出数据4.数组5.超全局变量6.文件操作 php的命…

LangChain 工作流编排

文章目录 LCEL流式调用案例invoke的异步调用异步流中的事件 LCEL LangChain Expression Language&#xff0c;是一种强大的工作流编排工具&#xff0c;可以从基本组件构建复杂的任务链&#xff08;Chain&#xff09;&#xff0c;有如下亮点&#xff1a; 流式支持&#xff1b;…

PyTorch 深度学习实战(14):Deep Deterministic Policy Gradient (DDPG) 算法

在上一篇文章中&#xff0c;我们介绍了 Proximal Policy Optimization (PPO) 算法&#xff0c;并使用它解决了 CartPole 问题。本文将深入探讨 Deep Deterministic Policy Gradient (DDPG) 算法&#xff0c;这是一种用于连续动作空间的强化学习算法。我们将使用 PyTorch 实现 D…

3.14-1列表

列表 一.列表的介绍和定义 1 .列表 类型: <class list> 2.符号:[] 3.定义列表: 方式1:[] 通过[] 来定义 list[1,2,3,4,6] print(type(list)) #<class list> 方式2: 通过list 转换 str2"12345" print(type(str2)) #<class str> list2lis…

Java集合 - HashMap

HashMap 是 Java 集合框架中的一个重要类&#xff0c;位于 java.util 包中。它实现了 Map 接口&#xff0c;基于哈希表的数据结构来存储键值对&#xff08;key-value pairs&#xff09;。HashMap 允许使用 null 作为键和值&#xff0c;并且是非同步的&#xff08;非线程安全的&…

有效的山脉数组 力扣941

一、题目 给定一个整数数组 arr&#xff0c;如果它是有效的山脉数组就返回 true&#xff0c;否则返回 false。 让我们回顾一下&#xff0c;如果 arr 满足下述条件&#xff0c;那么它是一个山脉数组&#xff1a; arr.length > 3在 0 < i < arr.length - 1 条件下&am…

本地部署Spark集群

部署Spark集群大体上分为两种模式&#xff1a;单机模式与集群模式 大多数分布式框架都支持单机模式&#xff0c;方便开发者调试框架的运行环境。但是在生产环境中&#xff0c;并不会使用单机模式。 下面详细列举了Spark目前支持的部署模式。 &#xff08;1&#xff09;Local…

前端---初识HTML(前端三剑客)

1.HTML 先为大家介绍几个学习前端的网站&#xff1a;菜鸟教程&#xff0c;w3school&#xff0c;CSS HTML&#xff1a;超文本标记语言 超⽂本: ⽐⽂本要强⼤. 通过链接和交互式⽅式来组织和呈现信息的⽂本形式. 不仅仅有⽂本, 还可能包含图⽚, ⾳频, 或者⾃已经审阅过它的学者…

AcWing 4905. 面包店 二分

类似还有一个题是二分&#xff0c;用区间来判断是否有解 这个爆long long 有点坑了 const int N 1e2 10;LL n,tc,Tm; LL a[N],b[N],c[N];bool check(LL mid) {LL minx max(0LL,mid 1 - Tm),maxx min(tc - 1LL,mid);//将y转为x的函数,此时判断x是否有解//枚举所有客户的需…

SpringBoot 第一课(Ⅲ) 配置类注解

目录 一、PropertySource 二、ImportResource ①SpringConfig &#xff08;Spring框架全注解&#xff09; ②ImportResource注解实现 三、Bean 四、多配置文件 多Profile文件的使用 文件命名约定&#xff1a; 激活Profile&#xff1a; YAML文件支持多文档块&#xff…

2025年西安交通大学少年班招生考试初试数学试题(初中组)

1、已知正整数 x 、 y 、 z x、y、z x、y、z 满足 x y z 2025 xyz2025 xyz2025 &#xff0c; x 2 y y 2 z z 2 x x y 2 y z 2 z x 2 x^2yy^2zz^2xxy^2yz^2zx^2 x2yy2zz2xxy2yz2zx2&#xff0c;则 x 、 y 、 z x、y、z x、y、z 共有 ___ 组解。 2、在数 1 、 2 、 …

开发、科研、日常办公工具汇总(自用,持续更新)

主要记录汇总一下自己平常会用到的网站工具&#xff0c;方便查阅。 update&#xff1a;2025/2/11&#xff08;开发网站补一下&#xff09; update&#xff1a;2025/2/21&#xff08;补充一些AI工具&#xff0c;刚好在做AI视频相关工作&#xff09; update&#xff1a;2025/3/7&…

软件架构设计习题及复习

软件系统需求分析 系统需求模型转换为架构模型 软件架构设计 架构风格领域 难点 单选 平衡点是敏感点的一种&#xff0c;如果达到了平衡点一定要选平衡点&#xff0c;不能选敏感点添加层次不能提高系统性能&#xff0c;任何时候直接沟通性能最高效

ccf3501密码

//密码 #include<iostream> #include<cstring> using namespace std; int panduan(char a[]){int lstrlen(a);int s0;int zm0,sz0,t0;int b[26]{0},c[26]{0},d[10]{0},e0,f0;while(s<l&&l>6){if(a[s]<Z&&a[s]>A){b[a[s]-A];zm;}if(a[s…

【JavaEE进阶】Spring事务

目录 &#x1f343;前言 &#x1f334;事务简介 &#x1f6a9; 什么是事务? &#x1f6a9;为什么需要事务? &#x1f6a9;事务的操作 &#x1f340;Spring 中事务的实现 &#x1f6a9;Spring 编程式事务 &#x1f6a9;Spring声明式事务Transactional &#x1f6a9;T…

MySQL索引特性——会涉及索引的底层B+树

1 没有索引&#xff0c;可能会有什么问题 索引&#xff1a;提高数据库的性能&#xff0c;索引是物美价廉的东西了。不用加内存&#xff0c;不用改程序&#xff0c;不用调sql&#xff0c;只要执行正确的 create index &#xff0c;查询速度就可能提高成百上千倍。但是天下没有免…

给单片机生成字库的方案

Python 这段代码用来将txt文件中储存的字符串转变成二进制的像素数据 from PIL import Image, ImageFont, ImageDraw import osdef find_microsoft_yahei():"""Windows系统定位微软雅黑字体"""font_paths ["C:/Windows/Fonts/msyh.ttc&q…

01Spring Security框架

Spring Security是什么&#xff1f; Spring Security是⼀个提供身份验证、授权和针对常见攻击的保护的框架。 Spring Security做什么&#xff1f; 作为开发⼈员&#xff0c;在⽇常开发过程中需要⽤到Spring Security的场景⾮常多。事实上&#xff0c;对Web应⽤程序⽽⾔&#xf…

「BWAPP靶场通关记录(1)」A1注入漏洞

BWAPP通关秘籍&#xff08;1&#xff09;&#xff1a;A1 injection 1.HTML Injection - Reflected (GET)1.1Low1.2Medium1.3High 2.HTML Injection - Reflected (POST)2.1Low2.2Medium2.3High 3.HTML Injection - Reflected (URL)3.1Low3.2/3.3Medium/HIgh 4.HTML Injection - …

机器学习算法实战——敏感词检测(主页有源码)

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​ ​​​ 1. 引言 随着互联网的快速发展&#xff0c;信息传播的速度和范围达到了前所未有的高度。然而&#xff0c;网络空间中也充斥着大量的…