Qt之美(一):d指針/p指針詳解

[/b]2011.11.16
[size=18px;] 首先,看了Xizhi Zhu 的這篇[url=http://blog.csdn.net/zhu_xz/article/details/6035861]Qt之美(一):D指針/私有實現[/url],對於很多批評不美的同路人,暫且不去評論,只是想支持一下Xizhi Zhu,在引用一下Jerry Sun的話,“C++需要宏定義就像需要設計模式一樣。也許你不知道,宏是圖靈完全(turing complete)的,至少LISP下是這樣,C/C++需要宏,幾乎所有重要的C/C++庫都需要和依賴宏。這些都超過咱們的想象,宏能帶給我們所謂語法糖(Syntax
sugar)的方便。如果你不理解,並且不能熟練使用宏,內聯函數和通用模板,那麼你還和熟練的C++程序員有一定距離。”<br>
這裏不去評論Jerry Sun的理解,有關宏是否圖靈完全,對實際編程也沒有啥意義的。至少我們看到Qt用了不少。閒話少敘,書歸正文。[/size]

[size=18px;][b]1.二進制兼容性[/b][/size]

[size=18px;] 這裏,先簡單解釋一下什麼破壞了代碼的二進制兼容性(至於二進制兼容性是什麼,相信Xizhi Zhu的文章和KDE上的[url=http://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B]這篇文章[/url],已經說的很清楚了,有時間的話再翻譯一下)。換句話說,在對程序做了什麼樣的改變需要我們重新編譯呢?看下面的例子:[/size]

<textarea readonly name="code" class="cpp">class Widget {

...

private:

Rect m_geometry;

};

class Label :public Widget {

...

String text()const{return m_text; }

private:

String m_text;

};

</textarea>[size=18px;]在這裏工程名爲CuteApp,Widget類包含一個私有成員變量<span style="text-align: left;">m_geometry[/size]<span style="text-align: left;">。我們編譯</span><span style="text-align: left;">Widget</span><span style="text-align: left;">類,並且將其發佈爲</span>WidgetLib 1.0。<span style="margin: 0px; width: auto!important; float: none!important; height: auto!important; vertical-align: baseline!important;">對於WidgetLib
1.1版本,我們希望加入對樣式表的支持。在Widget類中我們相應的加入了新的數據成員。</span></span>

<textarea readonly name="code" class="cpp">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;

} ;

</textarea>[size=18px;]經過上述改變後,我們發現工程CuteApp可以通過編譯,但是當運行調用WidgetLib1.0時,程序崩潰。<br>
爲什麼會運行出錯呢?<br>
是因爲我們在加入成員變量m_stylesheet後,改變了Widget和Label類的對象佈局。這是由於當編譯器在編譯程序時,它是用所謂的offsets來標記在類中的成員變量。我們將對象佈局簡化,其在內存中大致形象如下所示:[/size]

<code class="cpp plain" style="margin: 0px; width: auto!important; font-family: Consolas,'Bitstream Vera Sans Mono','Courier New',Courier,monospace!important; float: none!important; height: auto!important; vertical-align: baseline!important;"><span style="font-family: Consolas,'Bitstream Vera Sans Mono','Courier New',Courier,monospace;"><span style="font-family: Consolas,'Bitstream Vera Sans Mono','Courier New',Courier,monospace; line-height: 14px;"><span style="color: #363534;"><img alt="" src="http://hi.csdn.net/attachment/201111/16/0_1321421628H7dW.gif"></span></span></span></code>

<code class="cpp plain" style="margin: 0px; width: auto!important; font-family: Consolas,'Bitstream Vera Sans Mono','Courier New',Courier,monospace!important; float: none!important; height: auto!important; vertical-align: baseline!important;">[size=18px;]在WidegetLib
1.0中,Label類的成員變量m_text還在<offset 1>。被編譯器編譯後,將Label::text()方法解釋爲獲取Label對象的<offset 1>。而在WidegetLib 1.1中,由於添加新的數據成員,導致m_text的標記位變爲<offset 2>。由於工程沒有重新編譯,c++編譯器還會將在編譯和運行時的對象大小認爲一致。也就是說,在編譯時,編譯器爲Label對象按照其大小在內存上分配了空間。而在運行時,由於Widget中m_stylesheet的加入導致Label的構造函數重寫了已經存在的內存空間,導致了程序崩潰。[/size]</code>

<code class="cpp plain" style="margin: 0px; width: auto!important; font-family: Consolas,'Bitstream Vera Sans Mono','Courier New',Courier,monospace!important; float: none!important; height: auto!important; vertical-align: baseline!important;">[size=18px;]
所以只要版本已發佈,除非重新編譯工程,否則就不能更改類的結構和大小。那麼,爲了能夠爲原有類方便的引入新的功能,這就是Qt引入D指針的目的。[/size]</code>

<code class="cpp plain" style="margin: 0px; width: auto!important; font-family: Consolas,'Bitstream Vera Sans Mono','Courier New',Courier,monospace!important; float: none!important; height: auto!important; vertical-align: baseline!important;"></code><code class="cpp plain" style="margin: 0px; width: auto!important; font-family: Consolas,'Bitstream Vera Sans Mono','Courier New',Courier,monospace!important; float: none!important; height: auto!important; vertical-align: baseline!important;">[size=18px;][b]2.D指針[/b][/size]</code>

[size=18px;]<code class="cpp plain" style="margin: 0px; width: auto!important; font-family: Consolas,'Bitstream Vera Sans Mono','Courier New',Courier,monospace!important; float: none!important; height: auto!important; vertical-align: baseline!important;">[b]</strong></code>保持一個庫中的所有公有類的大小恆定的問題可以通過單獨的私有指針給予解決。這個指針指向一個包含所有數據的私有數據結構體。這個結構體的大小可以隨意改變而不會產生副作用,應用程序只使用相關的公有類,所使用的對象大小永遠不會改變,它就是該指針的大小。這個指針就被稱作D指針。[/size]

[size=18px;]</span><textarea readonly name="code" class="cpp">/* widget.h */
// 私有數據結構體聲明。 其定義會在 widget.cpp 或是
// widget_p.h,總之不能在此頭文件
class WidgetPrivate;

class Widget {
...
Rect geometry()const;
...
private:
// d指針永遠不能在此頭文件中被引用
// 由於WidgetPrivate沒有在此頭文件中被定義,
// 任何訪問都會導致編譯錯誤。
WidgetPrivate *d_ptr;
};

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

/* widget.cpp */
#include "widget_p.h"
Widget::Widget()
: d_ptr(new WidgetPrivate)// 初始化 private 數據 {
}

Rect Widget::geoemtry()const{
// 本類的d指針只能被在自己的庫內被訪問
return d_ptr->geometry;
}

/* label.h */
class LabelPrivate;
class Label :publicWidget {
...
String text();
private:
// 自己類對應自己的d指針
LabelPrivate *d_ptr;
};

/* label.cpp */
// 這裏將私有結構體在cpp中定義
struct LabelPrivate {
String text;
};

Label::Label()
: d_ptr(new LabelPrivate) {
}

String Label::text() {
return d_ptr->text;
}
</textarea><br>

[size=18px;]有了上面的結構,CuteApp就不會與d指針直接打交道。因爲d指針只能在WidgetLib中被訪問,在每一次對Widget修改之後都要對其重新編譯,私有的結構體可以隨意更改,而不需要重新編譯整個工程項目。[/size]

[size=18px;][b]3.D指針的其他好處<br>[/b]除了以上優點,d指針還有如下優勢:<br>
1.隱藏實現細節——我們可以不提供widget.cpp文件而只提供WidgetLib和相應的頭文件和二進制文件。<br>
2.頭文件中沒有任何實現細節,可以作爲API使用。<br>
3.由於原本在頭文件的實現部分轉移到了源文件,所以編譯速度有所提高。<br>
其實以上的點都很細微,自己跟過源代碼的人都會了解,qt是隱藏了d指針的管理和核心源的實現。像是在_p.h中部分函數的聲明,qt也宣佈在以後版本中將會刪除。(This file is not part of the Qt API. It exists purely as animplementation detail. This header file may change from version toversion without notice, or even be removed.)[/size]

[size=18px;][b]4.Q指針<br>[/b]到目前爲止,我們已經熟悉了指向私有結構體的d指針。而在實際中,往往它將包含私有方法(helper函數)。例如,LabelPrivate可能會有getLinkTargetFromPoint()(helper函數)以當按下鼠標時去找到相應的鏈接目標。在很多場合,這些helper函數需要訪問公有類,例如訪問一些屬於Label類或是其基類Widget的函數。<br>
比方說,一個幫助函數setTextAndUpdateWidget()可能會調用Widget::update()函數去重新繪製Widget。因此,我們同樣需要WidgetPrivate存儲一個指向公有類的q指針。<br>[/size]


<textarea readonly name="code" class="cpp">/* widget.h */
class WidgetPrivate;

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

/* widget_p.h */
struct WidgetPrivate {
// 初始化q指針
WidgetPrivate(Widget *q) : q_ptr(q) { }
Widget *q_ptr;// q-ptr指向基類API
Rect geometry;
String stylesheet;
};

/* widget.cpp */
#include "widget_p.h"
// 初始化 private 數據,將this指針作爲參數傳遞以初始化 q-ptr指針
Widget::Widget()
: d_ptr(new WidgetPrivate(this)) {
}

Rect Widget::geoemtry()const{

return d_ptr->geometry;
}

/* label.h */
class LabelPrivate;
class Label :publicWidget {
...
String text()const;
private:
LabelPrivate *d_ptr;};

/* label.cpp */
struct LabelPrivate {
LabelPrivate(Label *q) : q_ptr(q) { }
Label *q_ptr; //Label中的q指針
String text;
};

Label::Label()
: d_ptr(new LabelPrivate(this)) {
}

String Label::text() {
return d_ptr->text;
}</textarea><br>

[b][size=18px;]5.進一步優化[/size][/b]

[size=18px;]在以上代碼中,每產生一個Label對象,就會爲相應的LabelPrivate和WidgetPrivate分配空間。如果我們用這種方式使用Qt的類,那麼當遇到像QListWidget(此類在繼承結構上有6層深度),就會爲相應的Private結構體分配6次空間。<br>
在下面示例代碼中,將會看到,我們用私有類結構去實例化相應構造類,並在其繼承體系上全部通過d指針來初始化列表。[/size]


<textarea readonly name="code" class="cpp">/* widget.h */
class Widget {
public:
Widget();
...
protected:
// 只有子類會訪問以下構造函數
Widget(WidgetPrivate &d);// 允許子類通過它們自己的私有結構體來初始化
WidgetPrivate *d_ptr;
};

/* widget_p.h */
struct WidgetPrivate {
WidgetPrivate(Widget *q) : q_ptr(q) { }
Widget *q_ptr;
Rect 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);// 允許Label的子類通過它們自己的私有結構體來初始化
// 注意Label在這已經不需要d_ptr指針,它用了其基類的d_ptr
};

/* label.cpp */
#include "widget_p.h"

class LabelPrivate :public WidgetPrivate {
public:
String text;
};

Label::Label()
: Widget(*new LabelPrivate)//用其自身的私有結構體來初始化d指針
}

Label::Label(LabelPrivate &d)
: Widget(d) {
}</textarea>[size=18px;]這時候,我覺得我體會到了不一樣的感覺,有點意思了吧,說不美的,可以想個更好的解決方案麼?<br>
當我們建立一個Label對象時,它就會建立相應的LabelPrivate結構體(其是WidgetPrivate的子類)。它將其d指針傳遞給Widget的保護構造函數。這時,建立一個Label對象僅需爲其私有結構體申請一次內存。Label同樣也有一個保護構造函數可以被繼承Label的子類使用,以提供自己對應的私有結構體。[/size]

[b][size=18px;]6.將q-ptr和d-ptr轉換成正確類型[/size][/b]

[size=18px;]前面一步優化導致的副作用是q-ptr和d-ptr分別是Widget和WidgetPrivate類型。這就意味着下面的操作是不起作用的。[/size]

<textarea readonly name="code" class="cpp">void Label::setText(constString &text) {
// 不起作用的,因爲d_ptr是WidgetPrivate類型的,即使其指向LabelPrivate對象
d_ptr->text = text;
}</textarea>[size=18px;]所以爲了在子類能夠使用d指針,我們用static_cast來做強制轉換。[/size]

<textarea readonly name="code" class="cpp">void Label::setText(const String &text) {
LabelPrivate *d =static_cast<LabelPrivate *>(d_ptr);// cast to our private type
d->text = text;
}

</textarea>[size=18px;]爲了不讓所有地方都飄滿static_cast,我們才引入宏定義。[/size]

<textarea readonly name="code" class="cpp">


// global.h (macros)
#define DPTR(Class) Class##Private *d = static_cast<Class##Private *>(d_ptr)
#define QPTR(Class) Class *q = static_cast<Class *>(q_ptr)

// label.cpp
void Label::setText(constString &text) {
DPTR(Label);
d->text = text;
}

void LabelPrivate::someHelperFunction() {
QPTR(label);
q->selectAll();// 我們現在可以通過此函數來訪問所有Label類中的方法
}</textarea>[size=18px;]至於,Qt中的D指針和Q指針的具體形式以及相應的宏定義,這裏就不再重複,<span>Xizhi Zhu的文章中已經有寫,完整的d指針和q指針的程序實例程序如下:(結合信號和槽機制)[/size][/size]

[size=18px;]<span>[/size]</span>
[b][size=18px;]//d_ptr.h[/size][/b]

<textarea readonly name="code" class="cpp">#ifndef D_PTR_H
#define D_PTR_H

#include <QObject>

template <typename T> static inline T *GetPtrHelper(T *ptr) { return ptr; }

#define DECLARE_PRIVATE(Class) \
inline Class##Private* d_func() { return reinterpret_cast<Class##Private*>(GetPtrHelper(d_ptr)); } \
inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private*>(GetPtrHelper(d_ptr)); }\
friend class Class##Private;

#define DPTR(Class) Class##Private * const d = d_func()

class MyClassPrivate;

class MyClass : public QObject {
Q_OBJECT
public:
explicit MyClass(QObject *parent = 0);
virtual ~MyClass();
void testFunc();
protected:
MyClass(MyClassPrivate &d);

private:
MyClassPrivate * const d_ptr;
DECLARE_PRIVATE(MyClass);
MyClass(const MyClass&);
MyClass& operator= (const MyClass&);
};

#endif
</textarea>[b][size=18px;]//d_ptr.cpp<br>[/size][/b]

<textarea readonly name="code" class="cpp">#include "d_ptr.h"
#include "q_ptr.h"

MyClass::MyClass(QObject *parent) : QObject(parent),
d_ptr(new MyClassPrivate(this)) {}

MyClass::~MyClass() {
DPTR(MyClass);
delete d;
}

void MyClass::testFunc() {
DPTR(MyClass);
d->fool();
}</textarea><strong>[size=18px;]//q_ptr.h[/size][/b]

<textarea readonly name="code" class="cpp">#ifndef Q_PTR_H
#define Q_PTR_H


#include <QObject>
#include "d_ptr.h"

#define DECLARE_PUBLIC(Class) \
inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
friend class Class;

#define QPTR(Class) Class * const q = q_func()

class MyClassPrivate : public QObject
{
Q_OBJECT

public:
MyClassPrivate(MyClass *q, QObject *parent = 0);
virtual ~MyClassPrivate() {}

signals:
void testSgnl();

private slots:
void testSlt();

public:
void fool();

private:
MyClass * const q_ptr;
DECLARE_PUBLIC(MyClass);
};

#endif </textarea><br>
//q_ptr.cpp

<textarea readonly name="code" class="cpp">#include <stdio.h>
#include "q_ptr.h"

MyClassPrivate::MyClassPrivate(MyClass *q, QObject *parent) : QObject(parent), q_ptr(q) {
connect(this, SIGNAL(testSgnl()), this, SLOT(testSlt()));
}

void MyClassPrivate::fool() {
emit testSgnl();
}

void MyClassPrivate::testSlt() {
printf("This is a pimpl pattern sample implemented in qt's \"d_ptr, q_ptr\" way\n");
}
</textarea><br>
//main.cpp

<textarea readonly name="code" class="cpp">#include "q_ptr.h"

int main(/*int argc, char *argv[]*/) {
MyClass * d_ptr = new MyClass;
d_ptr->testFunc();
delete d_ptr;
while(1);
return 0;
}
</textarea><br><br><br><br>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章