聲明:
- 這篇文章所描述技術的環境: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.