Qt內部的d指標和q指標手把手教你實現

鬼谷子com發表於2021-02-08

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物件的構造,空間成本浪費,我們要進行鍼對性的改造。

    1. 我們是不是可以複用基類裡面的d_ptr
    2. 這樣的話我們要去掉子類裡面的d_ptr
    3. 我們要用WidgetPrivate* ->LabelPrivate *就要使用c++多型的性質,所以要構造必要條件
      1. 繼承
      2. 虛擬函式
  • 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:
    }
    

此版本包含幾個修改點:

  1. 公開類新增一個protected級別的建構函式,用於子類構造的時候在初始化引數列表來初始化基類,這裡實現了多型的特性。
  2. 公開類的子類建構函式的初始化引數列表不再初始化d_ptr,而是呼叫基類的帶參建構函式,實參為*new LabelPrivate,跟[1]配合實現了多型性。
  3. d指標轉換成子型別的私有類才都呼叫相關的方法。
  4. Label子類也要實現一個protected保護級別的建構函式,因為Label也可能會被繼承。
  5. WidgetPrivate.h私有基類的解構函式定義為virtual,這樣在釋放資源的時候才能夠不漏掉LabelPrivate的釋放。
  6. 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>()就可以了。可以收工了~

相關文章