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

來源:互聯網
上載者:User

                                                               Translated  by  mznewfacer   2011.11.16   

     首先,看了Xizhi Zhu 的這篇Qt之美(一):D指標/私人實現,對於很多批評不美的同路人,暫且不去評論,只是想支援一下Xizhi Zhu,在引用一下Jerry Sun的話,“C++需要宏定義就像需要設計模式一樣。也許你不知道,宏是圖靈完全(turing complete)的,至少LISP下是這樣,C/C++需要宏,幾乎所有重要的C/C++庫都需要和依賴宏。這些都超過咱們的想象,宏能帶給我們所謂文法糖(Syntax
sugar)的方便。如果你不理解,並且不能熟練使用宏,內嵌函式和通用模板,那麼你還和熟練的C++程式員有一定距離。”
        這裡不去評論Jerry Sun的理解,有關宏是否圖靈完全,對實際編程也沒有啥意義的。至少我們看到Qt用了不少。閑話少敘,書歸本文。

1.二進位相容性

     這裡,先簡單解釋一下什麼破壞了代碼的二進位相容性(至於二進位相容性是什麼,相信Xizhi Zhu的文章和KDE上的這篇文章,已經說的很清楚了,有時間的話再翻譯一下)。換句話說,在對程式做了什麼樣的改變需要我們重新編譯呢?看下面的例子:

class Widget { ...private: Rect m_geometry;};class  Label :public Widget { ... String text()const{return m_text; }private: String m_text;};

在這裡工程名為CuteApp,Widget類包含一個私人成員變數m_geometry。我們編譯Widget類,並且將其發布為WidgetLib 1.0。對於WidgetLib
1.1版本,我們希望加入對樣式表的支援。在Widget類中我們相應的加入了新的資料成員。

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;} ; 

經過上述改變後,我們發現工程CuteApp可以通過編譯,但是當運行調用WidgetLib1.0時,程式崩潰。
為什麼會運行出錯呢?
是因為我們在加入成員變數m_stylesheet後,改變了Widget和Label類的物件版面配置。這是由於當編譯器在編譯器時,它是用所謂的offsets來標記在類中的成員變數。我們將物件版面配置簡化,其在記憶體中大致形象如下所示:

在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的建構函式重寫了已經存在的記憶體空間,導致了程式崩潰。

  
所以只要版本發行,除非重新編譯工程,否則就不能更改類的結構和大小。那麼,為了能夠為原有類方便的引入新的功能,這就是Qt引入D指標的目的。

2.D指標

  保持一個庫中的所有公有類的大小恒定的問題可以通過單獨的私人指標給予解決。這個指標指向一個包含所有資料的私人資料結構體。這個結構體的大小可以隨意改變而不會產生副作用,應用程式只使用相關的公有類,所使用的對象大小永遠不會改變,它就是該指標的大小。這個指標就被稱作D指標。

/* 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;} 

 

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

3.D指標的其他好處
除了以上優點,d指標還有如下優勢:
1.隱藏實現細節——我們可以不提供widget.cpp檔案而只提供WidgetLib和相應的標頭檔和二進位檔案。
2.標頭檔中沒有任何實現細節,可以作為API使用。
3.由於原本在標頭檔的實現部分轉移到了源檔案,所以編譯速度有所提高。
其實以上的點都很細微,自己跟過原始碼的人都會瞭解,qt是隱藏了d指標的管理和核心源的實現。像是在_p.h中部分函數的聲明,qt也宣布在以後版本中將會刪除。( 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.)

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

 

/* 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;}

5.進一步最佳化

在以上代碼中,每產生一個Label對象,就會為相應的LabelPrivate和WidgetPrivate分配空間。如果我們用這種方式使用Qt的類,那麼當遇到像QListWidget(此類在繼承結構上有6層深度),就會為相應的Private結構體分配6次空間。
在下面範例程式碼中,將會看到,我們用私人類結構去執行個體化相應構造類,並在其繼承體繫上全部通過d指標來初始化列表。

 

/* 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) {}

這時候,我覺得我體會到了不一樣的感覺,有點意思了吧,說不美的,可以想個更好的解決方案嗎?
當我們建立一個Label對象時,它就會建立相應的LabelPrivate結構體(其是WidgetPrivate的子類)。它將其d指標傳遞給Widget的保護建構函式。這時,建立一個Label對象僅需為其私人結構體申請一次記憶體。Label同樣也有一個保護建構函式可以被繼承Label的子類使用,以提供自己對應的私人結構體。

6.將q-ptr和d-ptr轉換成正確類型

前面一步最佳化導致的副作用是q-ptr和d-ptr分別是Widget和WidgetPrivate類型。這就意味著下面的操作是不起作用的。

void Label::setText(constString &text) {    // 不起作用的,因為d_ptr是WidgetPrivate類型的,即使其指向LabelPrivate對象    d_ptr->text = text;}

所以為了在子類能夠使用d指標,我們用static_cast來做強制轉換。

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

為了不讓所有地方都飄滿static_cast,我們才引入宏定義。

   // 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.cppvoid Label::setText(constString &text) {    DPTR(Label);    d->text = text;} void LabelPrivate::someHelperFunction() {    QPTR(label);    q->selectAll();// 我們現在可以通過此函數來訪問所有Label類中的方法}

至於,Qt中的D指標和Q指標的具體形式以及相應的宏定義,這裡就不再重複,Xizhi Zhu的文章中已經有寫,完整的d指標和q指標的程式執行個體程式如下:(結合訊號和槽機制)

 //d_ptr.h

#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_OBJECTpublic:    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 

//d_ptr.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();}

//q_ptr.h

#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_OBJECTpublic:    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 

//q_ptr.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");}

//main.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;}

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.