https://blog.csdn.net/mznewfacer/article/details/6976293
首先,看了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 */
-
-
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 */
-
-
// 初始化 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 */
-
-
-
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)
-
-
-
-
// 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
-
-
-
-
-
-
template <typename T> static inline T *GetPtrHelper(T *ptr) { return ptr; }
-
-
-
-
inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private*>(GetPtrHelper(d_ptr)); }\
-
friend class Class##Private;
-
-
-
-
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&);
-
};
-
-
//d_ptr.cpp
-
-
-
-
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
-
-
-
-
-
-
-
-
-
-
inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
-
friend class Class;
-
-
-
-
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);
-
};
-
-
//q_ptr.cpp
-
-
-
-
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
-
-
-
int main(/*int argc, char *argv[]*/) {
-
MyClass * d_ptr = new MyClass;
-
d_ptr->testFunc();
-
delete d_ptr;
-
while(1);
-
return 0;
-
}
