这一篇文章介绍QT框架中QT对象类型QObject类型的源代码在设计上的一个比较优秀的设计思想。

QObject类型定义

QObject

直接来看QObject的源代码。为了表达更简洁更直观,这里省略了跟本文无关的各种代码。

如果查看QObject类型定义,可以发现类型里面有很多成员函数,但是除了 d_ptr之外,就没有定义更多的成员变量。当然,没有定义更多成员变量并不等于QObject对象实例中就只有d_ptr这一个数据,实际上由于QObject中定义了虚函数,因此QObject对象实例中还有vptr,也就是指向虚函数表的指针。

现在让我们来讨论这么一个问题,QObject肯定是有内部状态数据的,那么内部状态数据保存在哪儿呢?实际上就是保存在d_ptr指向的QT对象数据对象实例中。d_ptr是QT中的一个范围指针,也就是说当QObject对象被销毁时,d_ptr指向的QT对象数据对象实例也会被销毁掉。

QObjectData

先来看QObjectData类型定义。

这个类型里面定义了一个QObject指针类型的变量q_ptr。在QT对象数据类型有关的各种成员函数中,如果想访问所属于的QObject对象的this指针或者成员函数,可以通过q_ptr这个指针来访问。
也就是说QObject和QObjectData之间相互持有了对方的对象实例的指针,实现了QT对象和QT对象数据的之间的双向访问。

QObjectPrivate

这个类型是QObjectData类型的派生类型。定义了QT对象的一些私有数据。

QT对象的实际对象数据到底是什么

QObject对象

先来看一下QObject类型对象实例中的对象数据是什么。

如果跟踪调试一下,可以看到QObject类型构造函数的源代码。

可以看到在构造函数中一开始就实现了QObject和QObjectPrivate对象实例的双向引用,当然严格来讲是双向的指针指向。

现在新的问题来了,QT框架中有很多具体的QT对象类型,虽然它们都是QObject的直接或间接派生类型,但是各自必然都具有各自类型的独特的私有数据。那么QT框架是如何实现这些类型的对象实例在构造时使用自己类型独特的私有数据的呢?

QThread是一个典型正面例证

直接看QThread类型定义:

然后看一下QThread构造函数:

QThread在构造对象实例时先构造一个QThreadPrivate作为自己的私有数据对象实例,然后让d_ptr指针指向这个私有数据对象实例。
实际上不同的QObject派生类型的对象实例中,都会让d_ptr指向自己独特的私有数据对象实例。也就是QObject的d_ptr和QObjectData的q_ptr这两个指针指向的对象实例之间存在双向相互持有对方指针的情况。

QWidget是一个反面特例

QT框架中除了像QThread这样严格遵守一个QT对象实例只包含一个d_ptr范围指针的情况之外,还有一些特例,比如QWidget就是一个不严格遵守这种情况的特例。

QWidget的类型定义:

看一下QWidget的构造函数:

尽管QWidget除了QObject的d_ptr指针之外还有一个自己的指针data,但是看起来似乎仍然是符合这种不定义具体数据成员之定义指针的这种设计思想的。实际上并非如此。来看一下QPaintDevice类型的定义。

显然QPaintDevice这个类型中除了一个指针之外还有非指针类型的成员变量painters。尽管QPaintDevice并非QObject派生类型,但是肯定是影响到了QWidget的内存布局。

问题根源和解决方案

问题根源

QT框架会使用d_ptr这么一个范围指针,当然QT框架中还有很多类型使用的是原始指针类型。使用这么一个指针类型显然会带来编码上的一些额外的工作量,比如不能直接访问成员变量,而只能通过指针间接访问。
既然使用指针吃力不讨好,为什么QT框架要使用这么一个指针呢?无利不起早,QT框架的设计者不可能吃饱了撑的没事找事。d_ptr必然是解决了一些实际问题才有存在的价值。

先来看问题是怎么产生的。

假定有一个类型定义在butianyunobject.h文件中。

这个类型直接把私有数据成员变量定义在ButianyunObject类型本身,这在一些应用场景下会带来一些问题。

首先是编译问题。
如果有10个cpp文件#include了butianyunobject.h文件,那么一旦对这个.h文件的数据成员变量做那么一丁点修改,在下一次编译这个项目时就会导致这个10个cpp文件全部都必须重新编译。这也就是直接将类型的数据成员变量定义在类型本身的缺点。

其次是依赖问题。
如果说带来的不必要的重新编译问题只是一个项目组内部的技术问题,影响范围有限,那么现在讨论的依赖问题可能影响范围会比较大一点。
考虑这样一个场景:这个代码出现在一个对外公开发布的动态链接库中,而且作为一个公开导出接口,很多客户项目产品中使用了这个公开导出接口。
现在库开发项目组中如果有人为了修复BUG,在某个新版本中修改了一下这个ButianyunObject类型的私有数据成员变量然后新版本中将这个动态链接库和对应的头文件公开发布出去,会产生什么问题呢?或者说会影响到客户项目产品码?
答案是一定会影响到使用新版本的客户项目产品。一个类型的数据成员变量修改之后,会导致类型的对象实例的内存布局发生变化,这意味着所有想使用新版本的客户项目产品的软件必须使用新的头文件重新编译,而无法直接使用新版本的动态链接库去替换旧版本的动态链接库。

解决方案

然后来看解决方案,或者说设计思路。
如果将ButianyunObject类型这么来定义就可以避免这些问题。

也就是将类型的私有数据成员变量全部抽取到一个私有数据类型中,然后将这个私有数据类型定义在.cpp文件中,而不是.h文件中。在对外公开的数据类型中只定义了一个指针类型的变量d_ptr, d_ptr指向实际的私有数据对象实例。
这样就算是修改私有数据成员变量,也不会导致对外公开接口发生任何变化,那么也就不会引起内部编译问题。在各种公开接口的内存布局没有发生变化的情况下,如果没有其它影响因素,那么也就不会引起客户项目产品软件的重新编译问题,也就是说客户项目组可能只需替换成新版本的动态链接库即可。

间接的设计思想

这种设计思想就是所说的C++ PIMPL。PIMPL=Pointer to Implement,也就是指向具体实现的指针,说白了就是使用指向对象的具体私有数据对象实例的指针代替直接定义私有数据本身。
QT框架中实际上大量使用了PIMPL设计思想。

这篇文章也可以说是受到了知乎上对C++ PIMPL的讨论的帖子的启发才编写的。
为什么C++ PImpl 的实现类成员函数的参数类型需要是回溯引用?

下面再进一步进行抽象思考,再拔高一个层次,所谓PIMPL,就是间接的思想的具体体现或者具体应用。实际上C/C++的指针类型的“指向”的含义本来自带一层“间接”的意思。
间接的思想是所有设计模式的最本质最根本的底层设计思想和底层思考逻辑。可以这么讲,没有哪一个设计模式不是对间接的思想的具体体现。

当然再拔高了讲,大部分软件架构也都是在某一个层面上充分应用了间接的思想。一般的思考逻辑也是将现状抽象一下,然后在某个点上使用一个软件框架来实现原来直接用几行代码去做的事情,也就是间接的使用一个更复杂的抽象逻辑框架去解决原来直接去做带来的问题。
总结

间接的思想带来的另外一个天然的好处就是自然而然的实现了关注点分离,对于公开接口的使用者而言,根本看不到一个框架或者类的内部实现细节。

当然,这里分析的只是QT框架核心模块的很少的一点源代码片段而已。有兴趣想深入学习QT原理的朋友可以关注一下这个课程:QT5原理与源码分析视频课程。
如果读者对如何快速全面了解QT框架感兴趣,可以看一下这篇文章:

bird:快速全面了解QT软件界面开发技术

如果读者对如何学习QT框架有兴趣,可以看一下这篇文章:

bird:如何学习C/C++/QT软件开发技术

如果您认为这篇文章对您有所帮助,请您一定立即点赞+喜欢+收藏,本文作者将能从您的点赞+喜欢+收藏中获取到创作新的好文章的动力。如果您认为作者写的文章还有一些参考价值,您也可以关注这篇文章的作者。

QT原理与源码分析之QT对象类型QObject源码中的间接的设计思想的更多相关文章

  1. JVM源码分析之Java对象头实现

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十一篇. 今天呢!灯塔君跟大家讲: JVM源码分析之Java对象头实现 HotSpot虚拟机中,对象在内存中的布局分为三 ...

  2. Spring AOP 源码分析 - 创建代理对象

    1.简介 在上一篇文章中,我分析了 Spring 是如何为目标 bean 筛选合适的通知器的.现在通知器选好了,接下来就要通过代理的方式将通知器(Advisor)所持有的通知(Advice)织入到 b ...

  3. Shiro源码分析之SecurityManager对象获取

    目录 SecurityManager获取过程 1.SecurityManager接口介绍 2.SecurityManager实例化时序图 3.源码分析 4.总结 @   上篇文章Shiro源码分析之获 ...

  4. Flask框架 (四)—— 请求上下文源码分析、g对象、第三方插件(flask_session、flask_script、wtforms)、信号

    Flask框架 (四)—— 请求上下文源码分析.g对象.第三方插件(flask_session.flask_script.wtforms).信号 目录 请求上下文源码分析.g对象.第三方插件(flas ...

  5. jQuery源码分析系列(36) : Ajax - 类型转化器

    什么是类型转化器? jQuery支持不同格式的数据返回形式,比如dataType为 xml, json,jsonp,script, or html 但是浏览器的XMLHttpRequest对象对数据的 ...

  6. TeamTalk源码分析(十一) —— pc客户端源码分析

           --写在前面的话  在要不要写这篇文章的纠结中挣扎了好久,就我个人而已,我接触windows编程,已经六七个年头了,尤其是在我读研的三年内,基本心思都是花在学习和研究windows程序上 ...

  7. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

  8. Springboot源码分析之代理对象内嵌调用

    摘要: 关于这个话题可能最多的是@Async和@Transactional一起混用,我先解释一下什么是代理对象内嵌调用,指的是一个代理方法调用了同类的另一个代理方法.首先在这儿我要声明事务直接的嵌套调 ...

  9. Netty源码分析 (七)----- read过程 源码分析

    在上一篇文章中,我们分析了processSelectedKey这个方法中的accept过程,本文将分析一下work线程中的read过程. private static void processSele ...

  10. Android源码分析(六)-----蓝牙Bluetooth源码目录分析

    一 :Bluetooth 的设置应用 packages\apps\Settings\src\com\android\settings\bluetooth* 蓝牙设置应用及设置参数,蓝牙状态,蓝牙设备等 ...

随机推荐

  1. 缓存框架 Caffeine 的可视化探索与实践

    作者:vivo 互联网服务器团队-  Wang Zhi Caffeine 作为一个高性能的缓存框架而被大量使用.本文基于Caffeine已有的基础进行定制化开发实现可视化功能. 一.背景 Caffei ...

  2. git篇-- Git在项目实操中常见的使用命令--02

    Git是现代软件开发中不可或缺的版本控制工具.它能帮助开发者跟踪项目的所有变更,并与团队成员高效协作.本文将介绍一些在项目实操中常见的Git命令,帮助你更好地管理代码. 1. 初始化和配置 初始化仓库 ...

  3. Known框架实战演练——进销存财务管理

    本文介绍如何实现进销存管理系统的财务对账模块,财务对账模块包括供应商对账和客户对账2个菜单页面.供应商和客户对账字段相同,因此可共用一个页面组件类. 项目代码:JxcLite 开源地址: https: ...

  4. 【Vue】01 基础语法

    Hello Vue的演示案例: <!DOCTYPE html> <html lang="en" xmlns:v-bind="http://www.w3. ...

  5. 【Docker】07 部署挂载本地目录的Tomcat

    1.拉取Tomcat镜像: docker pull tomcat:9.0.37 2.创建并运行Tomcat容器: 挂载容器的webapps目录到本机(宿主机)自己设置的目录 docker run -d ...

  6. 计算机领域:学术写作中的conducive的含义表示

    "Conducive" 的意思是"有助于"或"有益于".在学术和正式的写作中,"conducive" 常用于描述某种情况 ...

  7. 国产CPU,国产操作系统UOS——零刻LZX迷你主机 , 显卡驱动安装以及屏幕配置

    看网上新闻发现了一款mini电脑--零刻LZX迷你主机 国产兆芯四核八线程 教学家用办公全能王8+256G 该款电脑使用的是国产CPU兆芯,以及国产操作系统UOS,由于价格还不贵就入手玩玩. 商品地址 ...

  8. conda环境下安装nvidia-nvcc

    参考: https://www.cnblogs.com/littletreee/p/17234053.html conda安装Pytorch或TensorFlow的时候是默认不安装nvcc,但是有时候 ...

  9. ubuntu23.04/22.04下安装docker engine

    官方网址: https://docs.docker.com/engine/install/ubuntu/ 2023年12月1日更新 -- Ubuntu 23.04 # Add Docker's off ...

  10. 利用sql查出的结果集重新生成一张虚拟表

    "select * from ( SELECT mc.id,mc.sn,mc.updated,mc.client_name,mc.brand,mc.mileage,mc.displace,m ...