Qt核心剖析:信息隐藏
原文 http://devbean.blog.51cto.com/448512/326686
(一)
如果你阅读了 Qt 的源代码,你会看到一堆奇奇怪怪的宏,例如 Q_D,Q_Q。我们的Qt源码之旅就从理解这些宏说起。
下面先看一个C++的例子。
- class Person
- {
- public:
- Person(){}
- ~Person(){}
- string name();
- void setName(string name);
- int age();
- void setAge(int a);
- private:
- string _name;
- int _age;
- };
这是一个很普通的 C++ 类 Person,他有两个属性 name 和 age。我们试想一下,这个类要怎么去使用呢?如果你不想给我源代码,那么至少也要给我一个 dll 或者其他类似的东西,并且你要把这个头文件给我,这样我才能把它 include 到我的代码中使用。我只能使用你暴露给我的 public 的接口。按理说,private 的东西我是不应该知道的,但是现在我知道了!为什么呢?因为我会去读这个头文件,我知道了,原来在 Person 中,age 就是一个 int,name 就是一个 string。这是你不希望看到的,因为既然你把它声明成了 private 的,就是不想让我知道这些东西。那么怎么办呢?嗯,我有一个解决方案。来看下面的代码:
person.h
- class Person
- {
- public:
- Person(){}
- ~Person(){}
- string name();
- void setName(string name);
- int age();
- void setAge(int a);
- private:
- PersonPrivateData *data;
- };
persondata.cpp
- class PersonPrivateData
- {
- public:
- string name;
- int age;
- };
怎么样?在 person.h 中看不到我是怎么存储的数据了吧?嗯嗯,也许你很聪明:我还可以在 persondata.cpp 中找到那些声明啊!当然,这是C++语法规定的,我们已经左右不了——但是,我为什么非要把 cpp 文件一并给你呢?因为你使用我的类库的话完全不需要使用 cpp 文件啊。
这就是一种信息隐藏的方法。看上去很麻烦,原本很简单的对 name 和 age 的访问都不得不通过一个指针去访问它,何必呢?其实这样做是有好处的:
- 减少头文件的依赖。像这样我们把数据成员都写在 cpp 文件中,当我们需要修改数据成员的时候就只需要修改 cpp 文件。虽然都是修改,但这和修改 .h 文件是不一样的!原因在于,如果 .h 文件发生改变,编译器会重新编译所有 include 了这个 .h 文件的文件。如果你这个类相当底层,那就会花费很长时间。
- 增加类的信息封装。这意味着你根本看不到具体数据类型,必须使用 getter 和 setter 去访问。我们知道 C++ 有一个 typedef 语句,我定义一个数据类型 ABC,如果你看不到具体文件,你会知道这个 ABC 是 string 还是 int 么?
这就是 C++ 的一种设计方法,被称为 Private Class,大约就是私有类吧!更确切地说应该是私有数据类。据说,这也是 Qt 2.x 的实现方式。但是如果你去看你的 Qt SDK 代码,你是看不到这样的语句的,取而代之的则是一些我们开头所说的 Q_D 这些宏。或许你已经隐隐约约地猜到了,这些宏就是实现这个的:Private Data。
(二)
下面在上一篇的基础上,我们进入Qt的源代码,看看Qt4.x是如何实现 Private Classes 的。
正如前面我们说的,或许你会看到很多类似 Q_D 或者 Q_Q 这类的宏。那么,我们来试着看一下这样的代码:
- void MyClass::setFoo( int i )
- {
- Q_D(MyClass);
- d->m_foo = i;
- }
- int MyClass::foo() const
- {
- Q_D(const MyClass);
- return d->m_foo;
- }
按照传统 C++ 的类,如果我们要实现这样的 getter 和 setter,我们应该使用一个私有变量 _i,然后操作这个变量。按照上一篇说的 Private Class 的做法,我们就要建一个 MyClassPrivateData 这样的类,然后使用指针对所有的数据操作进行委托。
再来看一个比较 Qt 的例子:
- class MyObject: public QObject
- {
- Q_OBJECT
- public:
- MyObject();
- virtual ~ MyObject();
- void setMemberX( int x );
- int memberX() const;
- void setMemberY( double y);
- double memberY() const;
- signals:
- void priorityChanged( MyObject::Priority priority );
- private:
- int m_memberX;
- double m_memberY;
- };
在来看一下 Qt 的实现:
- class MyObjectPrivate;
- class MyObject: public QObject
- {
- Q_OBJECT
- public:
- MyObject();
- virtual ~ MyObject();
- void setMemberX( int x );
- int memberX() const;
- void setMemberY( double y);
- double memberY() const;
- signals:
- void priorityChanged( MyObject::Priority priority );
- protected:
- MyObjectPrivate * const d_ptr;
- private:
- Q_DECLARE_PRIVATE(MyObject);
- };
这个例子很简单,一个使用传统方法实现,另一个采用了 Qt4.x 的方法。Qt4.x 的方法被称为 D-Pointer,因为它会使用一个名为 d 的指针,正如上面写的那个 d_ptr。使用传统方法,我们需要在 private 里面写上所有的私有变量,通常这会让整个文件变得很长,更为重要的是,用户并不需要这些信息。而使用 D-Pointer 的方法,我们的接口变得很漂亮:再也没有那一串长长的私有变量了。你不再需要将你的私有变量一起发布出去,它们就在你的 d 指针里面。如果你要修改数据类型这些信息,你也不需要去修改头文件,只需改变私有数据类即可。
需要注意的一点是,与单纯的 C++ 类不同,如果你的私有类需要定义 signals 和 slots,就应该把这个定义放在头文件中,而不是像上一篇所说的放在 cpp 文件中。这是因为 qmake 只会检测 .h 文件中的 Q_OBJECT 宏
(这一点大家务必注意)。当然,你不应该把这样的 private class 放在你的类的同一个头文件中,因为这样做的话就没有意义了。常见做法是,定义一个 private 的头文件,例如使用 myclass_p.h 的命名方式(这也是 Qt 的命名方式)。并且记住,不要把 private 头文件放到你发布的 include 下面!因为这不是你发布的一部分,它们是私有的。然后,在你的 myclass 头文件中,使用
- class MyClassPrivate;
这种前向声明而不是直接
- #include "myclass_p.h"
这种方式。这也是为了避免将私有的头文件发布出去,并且前向声明可以缩短编译时间。
在这个类的 private 部分,我们使用了一个 MyClassPrivate 的 const 指针 d_ptr。如果你需要让这个类的子类也能够使用这个指针,就应该把这个 d_ptr 放在 protected 部分,正如上面的代码那样。并且,我们还加上了 const 关键字,来确保它只能被初始化一次。
下面,我们遇到了一个神奇的宏:Q_DECLARE_PRIVATE。这是干什么用的?那么,我们先来看一下这个宏的展开:
- #define Q_DECLARE_PRIVATE(Class) \
- inline Class##Private* d_func() { return reinterpret_cast(qGetPtrHelper(d_ptr)); } \
- inline const Class##Private* d_func() const { return reinterpret_cast(qGetPtrHelper(d_ptr)); } \
- friend class Class##Private;
如果你看不大懂,那么就用我们的 Q_DECLARE_PRIVATE(MyClass) 看看展开之后是什么吧:
- inline MyClassPrivate* d_func() { return reinterpret_cast(qGetPtrHelper(d_ptr)); }
- inline const MyClassPrivate* d_func() const { return reinterpret_cast(qGetPtrHelper(d_ptr)); }
- friend class MyClassPrivate;
它实际上创建了两个 inline 的 d_func() 函数,返回值分别是我们的 d_ptr 指针和 const 指针。另外,它还把 MyClassPrivate 类声明为 MyClass 的 friend。这样的话,我们就可以在 MyClass 这个类里面使用 Q_D(MyClass) 以及 Q_D(const MyClass)。还记得我们最先看到的那段代码吗?现在我们来看看这个 Q_D 倒是是何方神圣!
- // A template function for getting the instance to your private class instance.
- template static inline T *qGetPtrHelper(T *ptr) { return ptr; }
- // A macro for getting the d-pointer
- #define Q_D(Class) Class##Private * const d = d_func()
下面还是自己展开一下这个宏,就成了
- MyClassPrivate * const d = d_func()
简单来说,Qt 为我们把从 d_func() 获取 MyClassPrivate 指针的代码给封装起来了,这样我们就可以比较面向对象的使用 getter 函数获取这个指针了。
现在我们已经比较清楚的知道 Qt 是如何使用 D-Pointer 实现我们前面所说的信息隐藏的了。但是,还有一个问题:如果我们把大部分代码集中到 MyClassPrivate 里面,很可能需要让 MyClassPrivate 的实现访问到 MyClass 的一些东西。现在我们让主类通过 D-Pointer 访问 MyClassPrivate 的数据,但是怎么反过来让 MyClassPrivate 访问主类的数据呢?Qt 也提供了相应的解决方案,那就是 Q_Q 宏,例如:
- class MyObjectPrivate
- {
- public:
- MyObjectPrivate(MyObject * parent):
- q_ptr( parent ),
- m_priority(MyObject::Low)
- {}
- void foo()
- {
- // Demonstrate how to make MyObject to emit a signal
- Q_Q(MyObject);
- emit q->priorityChanged( m_priority );
- }
- // Members
- MyObject * const q_ptr;
- Q_DECLARE_PUBLIC(MyObject);
- MyObject::Priority m_priority;
- };
在 private 类 MyObjectPrivate 中,通过构造函数将主类 MyObject 的指针传给 q_ptr。然后我们使用类似主类中使用的 Q_DECLARE_PRIVATE 的宏一样的另外的宏 Q_DECLARE_PUBLIC。这个宏所做的就是让你能够通过 Q_Q(Class) 宏使用主类指针。与 D-Pointer 不同,这时候你需要使用的是 Q_Pointer。这两个是完全相对的,这里也就不再赘述。
现在我们已经能够使用比较 Qt 的方式来使用 Private Classes 实现信息隐藏了。这不仅仅是 Qt 的实现,当然,你也可以不用 Q_D 和 Q_Q,而是使用自己的方式,这些都无关紧要。最主要的是,我们了解了一种 C++ 类的设计思路,这是 Qt 的源代码教给我们的。
(三)
前面我们已经看到了怎样使用标准的 C++ 代码以及 Qt 提供的 API 来达到信息隐藏这一目标。下面我们来看一下 Qt 是如何实现的。
还是以 QObject 的源代码作为例子。先打开 qobject.h,找到 QObjectData 这个类的声明。具体代码如下所示:
- QObjectData {
- public:
- virtual ~QObjectData() = 0;
- // others
- };
然后在下面就可以找到 QObject 的声明:
- class QObject
- {
- Q_DECLARE_PRIVATE(QObject)
- public:
- Q_INVOKABLE explicit QObject(QObject *parent=0);
- protected:
- QObject(QObjectPrivate &dd, QObject *parent = 0);
- QScopedPointer<QObjectData> d_ptr;
- // others
- }
注意,这里我们只是列出了我们所需要的代码,并且我的 Qt 版本是 2010.03。这部分代码可能会随着不同的 Qt 版本所有不同。
首先先了解一下 Qt 的设计思路。既然每个类都应该把自己的数据放在一个 private 类中,那么,为什么不把这个操作放在几乎所有类的父类 QObject 中呢?所以,Qt 实际上是用了这样一个思路,实现了我们前面介绍的数据隐藏机制。
首先回忆一下,我们前面说的 D-Pointer 需要有一个 private 或者 protected 的指向自己数据类的指针。在 QObject 中,
- QScopedPointer<QObjectData> d_ptr;
就扮演了这么一个角色。或许,你可以把它理解成
- QObjectData *d_ptr;
这不就和我们前面说的 D-Pointer 技术差不多了?QScopedPointer 是 Qt 提供的一个辅助类,这个类保存有一个指针,它的行为类似于一种智能指针:它能够保证在这个作用域结束后,里面的所有指针都能够被自动 delete 掉。也就是说,它其实就是一个比普通指针更多功能的指针。而这个指针被声明成 protected 的,也就是只有它本身以及其子类才能够访问到它。这就提供了让子类不必须声明这个 D-Pointer 的可能。
那么,前面我们说,QObjectData 这种数据类不应该放在公开的头文件中,可 Qt 怎么把它放进来了呢?这样做的用途是,QObject 的子类的数据类都可能继承自这个 QObjectData。这个类有一个纯虚的析构函数。没有实现代码,保证了这个类不能被初始化;虚的析构函数,保证了其子类都能够被正确的析构。
回到我们前面说明的 Q_DECLARE_PRIVATE 这个宏:
- #define Q_DECLARE_PRIVATE(Class) \
- inline Class##Private* d_func() { return reinterpret_cast(qGetPtrHelper(d_ptr)); } \
- inline const Class##Private* d_func() const { return reinterpret_cast(qGetPtrHelper(d_ptr)); } \
- friend class Class##Private;
我们把代码中的 Q_DECLARE_PRIVATE(QObject) 展开看看是什么东西:
- inline QObjectPrivate* d_func() { return reinterpret_cast(qGetPtrHelper(d_ptr)); }
- inline const QObjectPrivate* d_func() const { return reinterpret_cast(qGetPtrHelper(d_ptr)); }
- friend class QObjectPrivate;
清楚是清楚,只是这个 QObjectPrivate 是哪里来的?既然是 Private,那么它肯定不会在公开的头文件中。于是我们立刻想到到 qobject.cpp 或者是 qobject_p.h 中寻找。终于,我们在 qobject_p.h 中找到了这个类的声明:
- class Q_CORE_EXPORT QObjectPrivate : public QObjectData
- {
- Q_DECLARE_PUBLIC(QObject)
- public:
- QObjectPrivate(int version = QObjectPrivateVersion);
- virtual ~QObjectPrivate();
- // others
- }
这个类是继承 QObjectData 的!想想也是,因为我们说过,QObjectData 是不能被实例化的,如果要使用,必须创建它的一个子类。显然,QObjectPrivate 就扮演了这么一个角色了。不仅如此,我们还在这里看到了熟悉的 Q_DECLARE_PUBLIC 宏。好在我们已经知道它的含义了。
在 qobject.cpp 中,我们看一下 QObject 的构造函数:
- QObject::QObject(QObject *parent)
- : d_ptr(new QObjectPrivate)
- {
- // others
- }
- QObject::QObject(QObjectPrivate &dd, QObject *parent)
- : d_ptr(&dd)
- {
- // others
- }
第一个构造函数就是我们经常见到的那个。它使用自己创建的 QObjectPrivate 指针对 d_ptr 初始化。第二个构造函数使用传入的 QObjectPrivate 对象,但它是 protected 的,也就是说,你不能在外部类中使用这个构造函数。那么这个构造函数有什么用呢?我们来看一下 QWidget 的代码:
- class QWidget : public QObject, public QPaintDevice
- {
- Q_OBJECT
- Q_DECLARE_PRIVATE(QWidget)
- // others
- }
QWidget 是 QObject 的子类,然后看它的构造函数:
- QWidget::QWidget(QWidget *parent, Qt::WindowFlags f)
- : QObject(*new QWidgetPrivate, 0), QPaintDevice()
- {
- QT_TRY {
- d_func()->init(parent, f);
- } QT_CATCH(...) {
- QWidgetExceptionCleaner::cleanup(this, d_func());
- QT_RETHROW;
- }
- }
它调用了那个QObject 的 protected 构造函数,并且传入一个 QWidgetPrivate !这个 QWidgetPrivate 显然继承了 QObjectPrivate。于是我们已经明白,为什么 QWidget 中找不到 d_ptr 了,因为所有的 d_ptr 都已经在父类 QObject 中定义好了!尝试展开一下 Q_DECLARE_PRIVATE 宏,你就能够发现,它实际上把父类的 QObjectPrivate 指针偷偷地转换成了 QWidgetPrivate 的指针。这个就是前面说的 Qt 的设计思路。
Qt核心剖析:信息隐藏的更多相关文章
- Qt核心剖析:信息隐藏(三篇)
http://devbean.blog.51cto.com/448512/335550 http://devbean.blog.51cto.com/448512/325581 http://devbe ...
- Qt核心剖析: 寻找 QObject 的源代码
本来打算把<Qt学习之路>作为一个类似教程的东西,所以就不打算把一些关系到源代码的内容放在那个系列之中啦.因此今天就先来看一个新的开始吧!这个系列估计不会进展很快,因为最近公司里面要做 f ...
- Qt核心剖析: moc
前面我们说过,Qt 不是使用的“标准的” C++ 语言,而是对其进行了一定程度的“扩展”.这里我们从Qt新增加的关键字就可以看出来:signals.slots 或者 emit.所以有人会觉得 Qt 的 ...
- JavaScript设计模式——前奏(封装和信息隐藏)
前面一篇讲了js设计模式的前奏,包括接口的讲解.. 三:封装和信息隐藏: 信息隐藏用来进行解耦,定义一些私有的数据和方法. 封装是用来实现信息隐藏的技术,通过闭包实现私有数据的定义和使用. 接口在这其 ...
- QT核心编程之调试技术 (g)
Qt应用程序的调试可以通过DDD进行跟踪调试和打印各种调试或警告信息.DDD(Data Display Debugger)是使用gdb调试工具的图形工具,它安装在Linux操作系统中,使用方法可参考D ...
- Qt核心机制与原理
转: https://blog.csdn.net/light_in_dark/article/details/64125085 ★了解Qt和C++的关系 ★掌握Qt的信号/槽机制的原理和使用方法 ★ ...
- Qt核心机制和原理
转:http://blog.csdn.net/light_in_dark/article/details/64125085 ★了解Qt和C++的关系 ★掌握Qt的信号/槽机制的原理和使用方法 ★了解Q ...
- 如何封装JS ----》JS设计模式《------ 封装与信息隐藏
1. 封装与 信息隐藏之间的关系 实质是同一个概念的两种表达,信息隐藏式目的,二封装是借以达到目的的技术方法.封装是对象内部的数据表现形式和实现细节,要想访问封装过额对象中的数据,只有使用自己定义的操 ...
- Qt经典出错信息之undefined reference to `vtable for classname
原文链接:Qt经典出错信息之undefined reference to `vtable for classname 这个出错信息太常见了,用过Qt两个月以上的朋友基本上都能自己解决了,因为太经典了, ...
随机推荐
- element-ui的upload组件的clearFiles方法
<template> <div> <el-button @click="clearFiles">重新上传</el-button> & ...
- Network Initialization: Fan-in and Fan-out
https://github.com/pytorch/pytorch/blob/master/torch/nn/init.py @weak_script def _calculate_fan_in_a ...
- Python-Django学习笔记(一)-MTV设计模式
Django是开源的.大而且全的Web应用框架. 它独具特色,采用了MTV设计模式. MTV框架包括:Model(模型).Template(模板)和View(视图) Model(模型):负责业务对象与 ...
- Graph Regularized Feature Selection with Data Reconstruction
Abstract • 从图正则数据重构方面处理无监督特征选择: • 模型的思想是所选特征不仅通过图正则保留了原始数据的局部结构,也通过线性组合重构了每个数据点: • 所以重构误差成为判断所选特征质量的 ...
- [CF891C] Envy - Kruskal,并查集
给出一个 n 个点 m条边的无向图,每条边有边权,共 Q次询问,每次给出 \(k\)条边,问这些边能否同时在一棵最小生成树上. Solution 所有最小生成树中某权值的边的数量是一定的 加完小于某权 ...
- git的安装方法
下载:https://git-scm.com/downloads
- java高精度,大数
package 高精度幂; import java.math.BigDecimal; import java.util.Scanner; public class Main{ public stati ...
- 网易云信融合CDN方案及实践
日前,网易云信视频云架构师席智勇在第七届GFIC全球家庭互联网大会进行了题为<网易云信融合CDN方案及实践>的分享,以下是演讲内容回顾. 图为 网易云信视频云架构师席智勇 CDN所面临的问 ...
- Tomcat/conf/server.xml文件中docBase和path的说明
Tomcat的项目部署方式有以下三种: 1.直接把项目复制到Tomcat安装目录的webapps目录中,这是最简单的一种Tomcat项目部署的方法.2.在tomcat安装目录中有一个conf文件夹,打 ...
- C# ODP.Net oracle数据库操作 支持不安装客户端
下载: http://download.oracle.com/otn/other/ole-oo4o/ODTwithODAC1110720.zip?AuthParam=1414811820_e61f2f ...