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>()
就可以了。可以收工了~