最近接触C++动态链接库开发,遇到了动态链接库的二进制兼容问题,首先引入二进制兼容的概念:
A library is binary compatible, if a program linked dynamically to a former version of the library continues running with newer versions of the library without the need to recompile.
大概意思是:如果应用程序在动态链接某library的前后两个版本时不需要重新编译,则称这个library是二进制兼容的。
(另外还有个源码兼容的概念:比二进制兼容稍弱一些,即除了链接某library的前后两个版本时需要重新编译之外,再不需要其他任何改动,则称这个library是源码兼容的。Source compatible)
1. 引入D指针以保证动态链接库代码的二进制兼容性
我们先看一下什么情况下会造成二进制不兼容。
比如我们有个test.dll的动态链接库,它包含一个Foo类和其子类FooC,定义如下:
class TESTDLL_API Foo { //Foo.h
public:
Foo();
double getCurrent();
private:
double m_current;
}
class TESTDLL_API FooC : public Foo { //FooC.h
public:
FooC();
double getFuture();
private:
double m_future;
}
将Foo和FooC发布成test.dll后,供应用程序app.exe调用。
此时,需求说要在Foo类中加入一个新的成员变量,m_previous,那么此时Foo类的定义是:
class TESTDLL_API Foo { //Foo.h
public:
Foo();
double getCurrent();
double getPrevious();
private:
double m_current;
double m_previous;
}
子类FooC类的定义不变,再发布成test.dll,供应用程序app.exe调用(假设app.exe没有重新编译),那么对于app.exe来说,FooC类的内存布局中m_future的相对偏移为x,而实际上由于Foo中加入了成员变量m_previous,导致运行时FooC类的内存布局中m_future的相对偏移不再是x了。这样就会造成运行时错误(经实验,成员方法getPrevious()的加入并没有影响内存布局),test.dll不具备二进制兼容性。D指针在这种情况下闪亮登场!
首先需要为Foo类添加一个FooPrivate类来定义原来在Foo类中的成员变量(XXXPrivate是惯用的命名方式):
class FooPrivate { //Foo_p.h
public:
FooPricate();
private:
m_current;
}
然后修改Foo.h为:
class FooPrivate;
class TESTDLL_API Foo { //Foo.h
public:
Foo();
private:
FooPrivate *d_ptr;
}
最后如需要添加新的成员变量,则在FooPrivate类中添加,Foo类只有一个指向FooPrivate的D指针,这样就不会改变子类FooC类的内存布局,就可以保证test.dll的二进制兼容性了。
2. 引入Q指针以实现Private类对父类或者公有类方法的访问
在引入作为D指针的定义类(FooPrvate)后,如遇到FooPrivate访问公有类或者包含D指针的类(Foo)的需求,例如 对成员变量修改后调用父类的方法,这时可以在FooPrivate类中加入一个指向公有类或者Foo.class的q_ptr,称为Q指针,如下:
class FooPrivate { //Foo_p.h
public:
FooPricate(Foo *p_foo);
private:
m_current;
public:
Foo *q_ptr;
}
class FooCPrivate { //FooC_p.h
public:
FooCPrivate(FooC *p_fooc);
private:
double m_future;
public:
FooC *q_str;
}
FooPrivate和Foo以及FooCPrivate()和FooC()的构造函数分别为:
FooPrivate::FooPrivate(Foo *p_foo) : q_ptr(p_foo) { //Foo.cpp
...
}
Foo::Foo():d_ptr(new FooPrivate(this)) { //Foo.cpp
}
FooCPrivate::FooCPrivate(FooC *p_fooc) : q_str(p_fooc) { //FooC.cpp
...
}
FooC::FooC():d_ptr(new FooCPrivate(this)) { //FooC.cpp
}
这样就可以通过XXXPrivate类中的q_str调用父类的protect或public方法了。
3. 针对多层继承关系下Q、D指针造成的空间浪费优化
但是这种用法会带来一个问题:在拥有多层继承关系的类中,子类创建一个实例后,会额外创建继承路径上所有private类,造成空间浪费,如QListWidget(此类在继承结构上有6层深度),就会为相应的Private类分配6次空间。
因此需要对其进行优化,优化后子类创建一个实例后,只会创建自身对应的private类对象和根private类对象,具体做法如下:
2.1 所有private类都继承根private类;
2.2 去掉各子类中d_ptr,均使用根private类的对象作为隐式D指针;
2.3 去掉各子类中q_str,均使用根private类的Q指针指向根类(仅保留根private类的q_str);
2.4 根类加入protect构造函数,如Foo(FooPrivate &d),允许子类传入自己的private类来初始化。
优化后的FooPrivate、Foo、FooCPrivate、FooC如下所示(FooC继承Foo,FooCPrivate继承FooPrivate):
class FooPrivate { //Foo_p.h
public:
FooPrivate();
FooPrivate(Foo *p_foo);
private:
double m_current;
public:
Foo *q_ptr;
};
class FooCPrivate : public FooPrivate { //FooC_p.h
public:
FooCPrivate();
private:
double m_future;
};
class FooPrivate;
class TESTDLL_API Foo { //Foo.h
public:
Foo(void);
protected:
//只有子类会访问以下构造函数
Foo(FooPrivate &d); // 允许子类通过它们自己的private类来初始化
FooPrivate *d_ptr;
};
class FooCPrivate;
class TESTDLL_API FooC : public Foo { //FooC.h
public:
FooC(void);
protected:
FooC(FooCPrivate &d);
};
由于所有private类都仅继承根private类,在使用q_str的时候要加上强制类型转换:
#define QPTR(Class) Class *q = static_cast<Class *>(q_ptr) //q_str实际指向private对应的类的对象,
//如FooCPrivate对应的类是FooC
同理,在使用d_str的地方要加上强制类型转换:
#define DPTR(Class) Class##Private *d = static_cast<Class##Private *>(d_ptr) //d_str实际指向类对应的private对象
//如FooC对应的FooCPrivate对象
这两个宏类似于QT中经常见到的Q_Q宏和Q_D宏。
最后附上源代码,使用vs2012编写,Foo*这些类在smcore项目中,main函数在TestConsole中,按照写作思路分成_NODPTR、_DPTR、_QPTR、_QPTR_OPT这四个宏,分别对应上文进行阐述,可以在vs项目 c++预处理 里面使用上述宏。至于smcore项目中Moon、Orbiter等类跟本文并无关系,可以忽略。貌似无法选择0积分上传资源,有会的同学请在评论里告知,谢谢。
https://download.csdn.net/download/haoxinhaoxin/10277398
————————————————
版权声明:本文为CSDN博主「codeswimmer」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/haoxinhaoxin/article/details/79473930