Qt d指針和p指針

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類型。這就意味着下面的操作是不起作用的。
所以爲了在子類能夠使用d指針,我們用static_cast來做強制轉換。

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.cpp
void 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_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 

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

原文鏈接:https://blog.csdn.net/mznewfacer/article/details/6976293

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章