Qt內部的d指針和q指針手把手教你實現
Qt內部的d指針和q指針
在講Qt的D指針之前讓我們來簡單的解釋一下D指針出現的目的,目的是什麼呢?保證模組間的二進位兼容。
什麼是二進位兼容呢,簡單說就是如果自己的程式使用了第三方模組,二進位兼容可以保證在修改了第三方模組之後,也就是已經改變了記憶體布局之後,自己的程式可以不用重新編譯就能夠兼容修改後的第三方模組。 二進位指的是編譯生成的.so或者dll庫,一旦程式編譯好之後類的記憶體布局就確定了,兼容性值得就是即使記憶體布局被改變也依然能夠通過原來的記憶體布局找到對應的成員變數,比較官方的解釋是:
二進位兼容:在升級庫文件的時候,不必重新編譯使用此庫的可執行文件或其他庫文件,並且程式的功能不被破壞。
一些詳細的介紹可以參考這個博文。
之前轉載的一篇文章介紹了多態場景下基類和子類的記憶體布局。
當我們知道二進位兼容這個問題存在之後,在沒有參考Qt等解決方案之前,用我們自己聰明的小腦袋瓜子想一想怎麼解決這個問題?調整私有成員變數的順序會造成二進位不兼容,那我們把一個類的私有成員單獨拿出來封裝成一個只有成員變數的類,然後用一個指針指向這個類對象是不是就可以了呢?很好,這種解決問題的思路很好,比直接百度不思考要強多了。
馬斯克為什麼這麼厲害,我記得在一個採訪中他提到了為什麼自己這麼牛逼,什麼事情都敢幹,回答是因為自己堅信第一性原理這個理論。簡單闡述就是:
如果一件事從理論上是可行的,那就可以去干
那我們得到啟發之後回到這個問題本身,已經有了對二進位兼容的定義,我們根據上面的分析得出結論,用指針的方式實現不就可以規避二進位不兼容的情況了嗎?我們先動手嘗試完成一個自己腦子裡的第一版實現:
- widget.h
class WidgetPrivate;
class Widget
{
public:
Widget():d_ptr(new WidgetPrivate){ d_ptr->q_ptr = this; }//1
~Widget(){ if(d_ptr) delete d_ptr; }
inline void setWidth(int width) { d_ptr->width = width; }
inline int getWidth() { return d_ptr->width; }
protected:
WidgetPrivate* d_ptr = nullptr;
}
- widgetPrivate.h
class Widget;
class WidgetPrivate
{
public:
WidgetPrivate(){}
~WidgetPrivate(){}
int width;
int height;
Widget* q_ptr;
}
1處的程式碼可以直接設置q指針的方式比較優雅,不然我們要修改widgetPrivate(Widget* widget): q_ptr(widget){}
為這樣的構造函數,使用的時候
Widget():d_ptr(new WidgetPrivate(this))
,顯然這種方式不夠優雅。這樣的話classPrivate類就看著非常乾淨,甚至把class替換成struct都可以~
總的來說看起來很完美,widgetPrivate
作為私有類我們在改動的時候並不會破壞widget
的二進位兼容性。然後呢,夠了嗎?我們知道Qt的GUI類是對象樹的結構,存在著多層次繼承結構(QObject<-QWidget<-QLabel …),也在此基礎上實現了記憶體半自動化管理的機制。我們如果加一個子類呢?動手試試
-
Label.h
class LabelPrivate; class Label:public Widget { public: Lable():d_ptr(new LabelPrivate){ d_ptr->q_ptr = this; } ~Lable(){ if(d_ptr) delete d_ptr; } inline void setIcon(std::string icon){ d_ptr->icon = icon; } inline std::string getIcon() { return d_ptr->icon; } protected: LabelPrivate* d_ptr = nullptr; }
-
LabelPrivate.h
class Label; class LabelPrivate { public: LabelPrivate(){} ~LabelPrivate(){} std::string icon; Label* q_ptr = nullptr; private: }
-
使用
Label label; label.setWidth(65); label.setHeight(100); label.setIcon("d:/image/prettyGirl");
我們把構造函數的過程列印出來
WidgetPrivate::WidgetPrivate()
Widget::Widget()
WidgetPrivate::WidgetPrivate()
LabelPrivate::LabelPrivate()
Label::Label()
我們可以看到
WidgetPrivate::WidgetPrivate()
構造函數被調用了兩次,因為子類Label
也有一個d_ptr指針。這還是只有一層繼承結構的時候,每多一層繼承結構都要平白無故的增添一個BaseClassPrivate對象的構造,空間成本浪費,我們要進行針對性的改造。- 我們是不是可以復用基類裡面的d_ptr
- 這樣的話我們要去掉子類裡面的d_ptr
- 我們要用WidgetPrivate* ->LabelPrivate *就要使用c++多態的性質,所以要構造必要條件
- 繼承
- 虛函數
-
Widget.h
class WidgetPrivate; class Widget { public: Widget():d_ptr(new WidgetPrivate){ d_ptr->q_ptr = this; } ~Widget(){ if(d_ptr) delete d_ptr; } inline void setWidth(int width) { WidgetPrivate* d = reinterpret_cast<WidgetPrivate*>(d_ptr); d->width = width; } inline int getWidth() { WidgetPrivate* d = reinterpret_cast<WidgetPrivate*>(d_ptr); return d->width; } protected: Widget(WidgetPrivate& wprivate):d_ptr(&wprivate){}//[1] WidgetPrivate* d_ptr = nullptr; }
-
Label.h
class LabelPrivate; class Label:public Widget { public: Lable():Widget(*new LabelPrivate){ d_ptr->q_ptr = this; }//[2] ~Lable(){ if(d_ptr) delete d_ptr; } inline void setIcon(std::string icon){ LabelPrivate* d = reinterpret_cast<LabelPrivate*>(d_ptr);//[3] d_ptr->icon = icon; } inline std::string getIcon() { LabelPrivate* d = reinterpret_cast<LabelPrivate*>(d_ptr); return d_ptr->icon; } protected: Label(LabelPrivate& lprivate):d_ptr(&lprivate){}//[4] }
-
WidgetPrivate.h
class Widget; class WidgetPrivate { public: WidgetPrivate(){} virtual ~WidgetPrivate(){}//[5] int width; int height; Widget* q_ptr; }
-
LabelPrivate.h
class Label; class LabelPrivate:public WidgetPrivate//[6] { public: LabelPrivate(){} ~LabelPrivate(){} std::string icon; Label* q_ptr = nullptr; private: }
此版本包含幾個修改點:
- 公開類添加一個protected級別的構造函數,用於子類構造的時候在初始化參數列表來初始化基類,這裡實現了多態的特性。
- 公開類的子類構造函數的初始化參數列表不再初始化d_ptr,而是調用基類的帶參構造函數,實參為*new LabelPrivate,跟[1]配合實現了多態性。
- d指針轉換成子類型的私有類才都調用相關的方法。
- Label子類也要實現一個protected保護級別的構造函數,因為Label也可能會被繼承。
- WidgetPrivate.h私有基類的析構函數定義為virtual,這樣在釋放資源的時候才能夠不漏掉LabelPrivate的釋放。
- LabelPrivate繼承WidgetPrivate,構成多態的基礎條件。
Ok,到這裡就基本完成了,可以不做修改的替換掉Qt的那一套d指針和q指針,哈哈哈(有點扯了。)論實用程度是夠了,但是論優雅程度跟Qt原生的還是有一定距離,我們添加一些語法糖和c++11的智慧指針來優化一下。
-
global.h
#define D_D(Class) Class##Private* d = reinterpret_cast<Class##Private*>(d_ptr.get()); #define Q_D(Class) Class* q = reinterpret_cast<Class*>(d_ptr.get());
-
label.h
class LabelPrivate; class Label:public Widget { public: Lable():Widget(*new LabelPrivate){ d_ptr->q_ptr = this; } ~Lable(){ if(d_ptr) delete d_ptr; } inline void setIcon(std::string icon){ D_D(Label)//[1] d->icon = icon; } inline std::string getIcon() { D_D(Label)//[2] return d->icon; } protected: Label(LabelPrivate& lprivate):d_ptr(&lprivate){} }
是不是優雅了很多.
智慧指針的話只需要替換Widget::d_ptr為
std::unique_ptr<Widget>()
就可以了。可以收工了~