现在的位置: 首页 > 综合 > 正文

QT Internal: 对象数据存储

2013年04月10日 ⁄ 综合 ⁄ 共 7729字 ⁄ 字号 评论关闭

声明:

  • 这篇文章所描述技术的环境:QT 4.4.0 Open Source Edition on Linux

  • 篇文章中的所有代码都只是示例性的代码,仅仅只是为了说明技术本身,所以只选取了最基本的代码片段,而抛弃了其它冗余的、不利于理解技术本身的代码。同
    时,这些代码片段也省略了一些常规的安全检查,完整性检查,等必要的工作。通常情况下没有实用价值,也无法直接编译通过。
  • 本文为kudev.net原创文章,如需引用,转载,请注明本文链接http://www.kudev.net/bbs/thread-2448-1-1.html
    ,谢谢。

对象数据存储, 背景

    我们知道,在C++中,几乎每一个类(class)中都需要有一些类的成员变量(class member variable),在通常情况下的做法如下:

class PersonalData
{
private:
    string mszName; // 姓名
    bool mbSex;    // 性别
    int mnAge;     // 年龄
};

就是在类定义的时候,直接把类成员变量定义在这里,您是不是这是这样做的呢?

在QT中,却几乎都不是这样做的,为了更容易理解QT是如何定义类成员变量的,我们先说一下QT 2.x 版本中的类成员变量定义方法,因为在 2.x 中的方法非常容易理解。然后在介绍 QT 4.4 中的类成员变量定义方法。

QT 2.x 中的方法

在定义class的时候(.h文件),只是定义一个成员数据指针,然后由这个指针指向一个数据成员对象,这个数据成员对象包含所有这个class的成员数据,然后在class的实现文件(.h文件)中,定义这个私有数据成员对象。示例代码如下:

// File name:  personalData.h

struct PersonalDataPrivate; // 声明私有数据成员类型

class PersonalData
{
public:
    PersonalData();   // constructor
    virtual ~PersonalData();  // destructor

    void setAge(const int);
    int getAge();

private:
    PersonalDataPrivate* d;
};

// File name:  personalData.cpp

struct PersonalDataPrivate  // 定义私有数据成员类型
{
    string mszName; // 姓名
    bool mbSex;    // 性别
    int mnAge;     // 年龄
};

// constructor
PersonalData::PersonalData()
{
    d = new PersonalDataPrivate;
};

// destructor
PersonalData::~PersonalData()
{
    delete d;
};

void PersonalData::setAge(const int age)
{
    if (age != d->mnAge)
        d->mnAge = age;
}

int PersonalData::getAge()
{
    return d->mnAge;
}

   在最初学习QT的时候,我也觉得这种方法很麻烦,但是随着使用的增多,我开始很喜欢这个方法了,而且,现在我写的代码,基本上都会用这种方法。具体说来,它有如下优点:

1, 减少头文件的依赖性
 
  把具体的数据成员都放到cpp文件中去,
这样,在需要修改数据成员的时候,只需要改cpp文件而不l需要头文件,这样就可以避免一次因为头文件的修改而导致所有包含了这个文件的文件全部重新编译
一次,尤其是当这个头文件是非常底层的头文件和项目非常庞大的时候优势明显。
    同时,也减少了这个头文件对其它头文件的依赖性。可以把只在数据成员中需要用到的在cpp文件中include一次就可以,而无需在头文件中include

2, 增强类的封装性
    这种方法增强了类的封装性,无法再直接存取类成员变量,而必须写相应的 get/set 成员函数来做这些事情。

于这个问题,仁者见仁,智者见智,每个人都有不同的观点。有些人就是喜欢把类成员变量都定义成public的,在使用的时候方便。只是我个人不喜欢这种方
法,当项目变得很大的时候,非常多的人一起在做这个项目的时候,自己所写的代码处于底层有非常多的人需要使用(#include)的时候,这个方法的弊端
就充分的体现出来了。

    还有,我不喜欢 QT 2.x 中把数据成员的变量名都定义成只有一个字母,d,看起来很不直观,尤其是在search的时候,很不方便。但是,QT kernel 中的确就是这么干的。

QT 4.4 中的方法


在 QT 4.4 中,类成员变量定义方法的出发点没有变化,只是在具体的实现手段上发生了非常打的变化,下面具体来看。

在 QT 4.4 中,使用了非常多的宏来做事,这凭空的增加了理解 QT code 的难度。就连在定义类成员数据变量这件事情上,也大量的使用了宏。

在这个版本中,类成员变量不再是给每一个class都定义一个私有的成员,而是把这一项common的工作放到了最基础的基类 QObject 中,然后定义了一些相关的方法的存取,好了,让我们进入具体的代码吧。

// file name: qobject.h

class QObjectData
{
public:
    virtual ~QObjectData() = 0;
    // 省略
};

class QObject
{
    Q_DECLARE_PRIVATE(QObject)

public:
    QObject(QObject *parent=0);

protected:
    QObject(QObjectPrivate &dd, QObject *parent = 0);

    QObjectData *d_ptr;
}

 
  这些代码就是在 qobject.h 这个头文件中的。在 QObject class
的定义中,我们看到,数据员的定义为:QObjectData *d_ptr; 定义成 protected
类型的就是要让所有的派生类都可以存取这个变量,而在外部却不可以直接存取这个变量。

    而 QObjectData 的定义却放在了这个头文件中,其目的就是为了要所有从QObject继承出来的类的成员变量也都相应的要在QObjectData这个class继承出来。而纯虚的析构函数又决定了两件事:

1, 这个class不能直接被实例化。换句话说就是,new QObjectData 这行代码一定会出错,compile的时候是无法过关的。
2, 当 delete 这个指针变量的时候,这个指针变量是指向的任意从QObjectData继承出来的对象的时候,这个对象都能被正确delete,而不会产生错误,诸如,内存泄漏之类的。

我们再来看看这个宏做了什么,Q_DECLARE_PRIVATE(QObject)
#define Q_DECLARE_PRIVATE(Class) /
    inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(d_ptr); } /
inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(d_ptr); } /
friend class Class##Private;


个宏主要是定义了两个重载的函数,d_func(),作用就是把在QObject这个class中定义的数据成员变量d_ptr安全的转换成为每一个具体
的class的数据成员类型指针。我们看一下在QObject这个class中,这个宏展开之后的情况,就一幕了然了。

Q_DECLARE_PRIVATE(QObject) 展开后,就是下面的代码:
    inline QObjectPrivate* d_func() { return reinterpret_cast<QObjectPrivate *>(d_ptr); }
   inline const QObjectPrivate* d_func() const
       { return reinterpret_cast<const QObjectPrivate *>(d_ptr); } /
    friend class QObjectPrivate;

宏展开之后,新的问题又来了,这个QObjectPrivate是从哪里来的?在QObject这个class中,为什么不直接使用QObjectData来数据成员变量的类型?

还记得我们刚才说过吗,QObjectData这个class的析构函数的纯虚的,这就说明这个class是不能实例化的,所以,QObject这个class的成员变量的实际类型,这是从QObjectData继承出来的,它就是QObjectPrivate !

下面就是这个class的定义:
class QObjectPrivate : public QObjectData
{
    Q_DECLARE_PUBLIC(QObject)

public:
    QObjectPrivate(int version = QObjectPrivateVersion);
    virtual ~QObjectPrivate();
// 省略
}

那么,如何证明QObjectPrivate是QObject实例化后的数据成员变量d_ptr 的类型呢?下面我们来看看QObject 的构造函数的实现,一幕了然。

// file name: qobject.cpp

QObject::QObject(QObject *parent)
    : d_ptr(new QObjectPrivate)
{
  // ………………………
}

QObject::QObject(QObjectPrivate &dd, QObject *parent)
    : d_ptr(&dd)
{
// …………………
}

这第二个构造函数干什么用的呢?
从第一个构造函数可以很清楚的看出来,QObject class 中的 d_ptr 指针将指向一个 QObjectPrivate
的对象,而QObjectPrivate这个class是从QObjectData继承出来的。
再来看第二个构造函数。
从 QObject class 的定义中,我们可以看到,这第二个构造函数是被定义为 protected
类型的,这说明,这个构造函数只能被继承的class使用,而不能使用这个构造函数来直接道桓鯭Object对象,也就是说,如果写一条下面的语句,
编译的时候是会失败的,
new QObject(*new QObjectPrivate, NULL)
为了看的更清楚,我们以QWidget这个class为例说明。
QWidget是QT中所有UI控件的基类,它直接从QObject继承而来,
class QWidget : public QObject, public QPaintDevice
{
    Q_OBJECT
    Q_DECLARE_PRIVATE(QWidget)
// .....................
}
我们看一个这个class的构造函数的代码:
QWidget::QWidget(QWidget *parent, Qt::WindowFlags f)
    : QObject(*new QWidgetPrivate, 0), QPaintDevice()
{
    d_func()->init(parent, f);
}
非常清楚,它调用了基类QObject的保护类型的构造函数,并且以 *new QWidgetPrivate
作为第一个参数传递进去。也就是说,基类(QObject)中的d_ptr指针将会指向一个QWidgetPrivate类型的对象,
再看QWidgetPrivate这个class的定义:
class QWidgetPrivate : public QObjectPrivate
{
    Q_DECLARE_PUBLIC(QWidget)
// .....................
}
好了,这就把所有的事情都串联起来了。
关于QWidget构造函数中的唯一的语句 d_func()->init(parent, f)
我们注意到在class的定义中有这么一句话:
Q_DECLARE_PRIVATE(QWidget)
我们前面讲过这个宏,当把这个宏展开之后,就是这样的:
    inline QWidgetPrivate* d_func() { return
reinterpret_cast<QWidgetPrivate *>(d_ptr); }
    inline const QWidgetPrivate* d_func() const        { return
reinterpret_cast<const QWidgetPrivate *>(d_ptr); }     friend
class QWidgetPrivate;
很清楚,它就是把QObject中定义的d_ptr指针转换为QWidgetPrivate类型的指针。
小结:
要理解QT
Kernel的code,就必须要知道QT中每一个Object内部的数据是如何保存的,而QT没有象我们平时写code一样,把所有的变量直接定义在类
中,所以,不搞清楚这个问题,我们就无法理解一个相应的class。
其实,在QT4.4中的类成员数据的保存方法在本质是与QT2.x中的是一样的,就是在class中定义一个成员数据的指针,指向成员数据集合对象(这里
是一个QObjectData或者是其派生类)。初始化这个成员变量的办法是定义一个保护类型的构造函数,然后在派生类的构造函数new
一个派生类的数据成员,并将这个新对象赋值给QObject的数据指针。在使用的使用,通过预先定义个宏里面的一个inline函数来把数据指针在安全类
型转换,就可以使用了。

Private Implementation

Introduction

Private Implementation
(aka. "pimpl") is a C++ programming idiom which can be used to reduce
compilation times, to enhance encapsulation and to retain binary
compatibility.

Pimpl built in Qt

Anyone who has ever peeked under the covers and looked into the sources of Qt has most likely noticed macros called Q_D
and Q_Q
. Along with Q_DECLARE_PRIVATE
and Q_DECLARE_PUBLIC
, these macros provide Qt's own pimpl mechanism.

QObject
,
the heart of the Qt object model, has a pointer to the private
implementation in it's interface. The private implementation pointer,
"d_ptr" cannot be set to anything custom as it would prevent QObject
from reaching it's private implementation which it strictly relies on.

Each and every private implementation class in Qt inherits QObjectPrivate
. For example QWidget
has QWidgetPrivate
and QPushButton
has QPushButtonPrivate
.
Where some of these private implementations are declared in separate
headers, others are not. This makes it, in most cases, impossible to
use aforementioned macros in one's own projects.

For example, let's say I'm subclassing QPushButton
. I would have to subclass the private implementation from QPushButtonPrivate
to avoid destroying QPushButton
's private implementation. But QPushButtonPrivate
is declared in qpushbutton.cpp
so it's basically impossible to subclass.

In addition to this, Qt's private headers/implementations are a subject to change. As it says in all the private headers:

//  W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.

In conclusion, the pimpl built in Qt is of no use for one's own
projects. It's not meant to be used and in many cases it's even
impossible.

How to build your own pimpl mechanism

The Internet is full of various pimpl examples. To put it in a
nutshell, you simply provide a pointer to the private implementation in
the private interface of a class. Instead of storing private members
directly to the class, place them into the private implementation
class.

One can save himself or herself from building redundant
mechanism from scratch to several classes by following the example of
the pimpl technique used in Qt. This can slightly reduce the amount of
handwork while declaring private implementation classes and later when
referencing them. The following example
makes use of the pimpl macros defined in Qt with only some minor modifications.

【上篇】
【下篇】

抱歉!评论已关闭.