将成员变量添加到共享库中的类中,不会破坏二进制兼容性吗

Add member variable to class in the shared library, will not break binary compatibility?

本文关键字:兼容性 二进制 添加 变量 成员 共享      更新时间:2023-10-16

我想找一个例子来表达我对二进制兼容性的理解,但失败了。我想通过在开始或中间添加类成员来改变DLL中类成员的布局,并期望变量不能正确访问或者访问该变量将产生崩溃。然而,一切都很顺利。我发现,无论我如何将成员变量添加到类的任何位置,都不会出现崩溃,也不会破坏二进制兼容性。我的代码如下:

//untitled1_global.h
#include <QtCore/qglobal.h>
#if defined(UNTITLED1_LIBRARY)
#  define UNTITLED1_EXPORT Q_DECL_EXPORT
#else
#  define UNTITLED1_EXPORT Q_DECL_IMPORT
#endif
//base.h
class UNTITLED1_EXPORT Base
{
public:
Base();
double getA();
double getB();
private:
int arr[100]; //Add later to update the DLL
double a;
double b;
};
//derived.h
#include "dpbase.h"
class UNTITLED1_EXPORT Derived :  public Base
{
public:
Derived();
void setC(double d);
double getC();
private:
char arrCh[100]; //Add later to update the DLL
double c;
};

下面是客户端代码,包含的base.hderived.h与DLL中的不一样,一个是注释的,一个不是。实现和声明在DLL中是分开的。我试图直接访问变量,并通过函数访问变量(例如在main.cpp开头注释(。

//main.cpp
#include "dpbase.h"
#include "dpbase2.h"
#include <QDebug>
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Base base;
qDebug() << base.getA();
qDebug() << base.getB();
Derived base2;
base2.setC(50);
qDebug() << base2.getC();
return a.exec();
}

其中,类BaseDeriveddll导出。无论我如何在BaseDerived的任何位置添加成员变量,都不会出现崩溃,也不会破坏二进制兼容性。

我正在使用qt。有一个这里有同样的问题,但对我没有帮助。

此外,我删除了DLL中类的所有成员变量,我仍然通过链接DLL使用客户端中不存在的变量,赋值,获取……即使没有定义成员变量,在动态库中似乎也有足够的空间可以由客户端重新定义。太奇怪了!

我的问题是,为什么更改DLL中类成员的布局不会破坏二进制兼容性?并删除DLL中类的所有成员var,但为什么调用方仍然可以使用.h文件中的成员?

首先,您必须了解如何访问成员。

让我们采用

class UNTITLED1_EXPORT Base
{
public:
Base();
double getA();
double getB();
private:
double a;
double b;
};

当您访问a时,就像执行*(this + 0)一样。当您访问b时,就像执行*(this + 8)一样(假设double是8个字节(。

然后当你这样改变你的等级时:

class UNTITLED1_EXPORT Base
{
public:
Base();
double getA();
double getB();
private:
int arr[100];
double a;
double b;
};

当您访问a时,它将执行*(this + 400),当您访问b时,它会执行*(this + 408)

现在,这可能是一个问题,也可能不是一个问题。如果您仅通过getA()getB()访问ab,并且它们是在DLL中的.cpp中定义的,那么您将在更新类的同时更新getter的定义。

您可以通过内联getter的定义来创建一些奇怪的行为:

class UNTITLED1_EXPORT Base
{
public:
Base();
double getA() { return a; }
double getB();
private:
double a;
double b;
};

在这种情况下,.exe可能有自己的getA()getB()副本。这意味着,在您更新DLL并添加int arr[400]后,您的.exe仍将尝试访问现在被arr占用的*(this + 0)

这是未定义的行为,但它不会使您的程序崩溃,因为您正在访问分配的内存。

如果你反其道而行之:

  1. 使用arr编译您的exe
  2. 删除arr
  3. 生成DLL
  4. 运行.exe

那么你更有可能发生车祸。因为exe将尝试访问this + 400,而Base只有16个字节。但由于多种原因,它仍然不能保证崩溃。例如,this + 400可能是有效的。但更重要的是,这取决于从哪里为Base分配内存。如果你在.exe中执行new Base,那么即使在你更改了DLL之后,它也会分配416个字节。但是,如果在DLL中执行new Base,它将只分配16个字节。


这是一个例子。

这是.exe:的标题

class Base
{
public:
Base();
double getA() const { return a; }
double getB() const { return b; }
static Base *create();
void print();
private:
double a;
double b;
};

DLL 的标头

类基础{公共:Base((;double getA((const{return a;}double getB((const;static Base*create((;void print((;私人:int arr【400】双a;双b;};

DLL的代码:

Base::Base()
{
for (int i = 0 ; i < 100 ; ++i) {
arr[i] = i;
}
a = 3.14;
b = 1.42;
}
double Base::getB() const { return b; }
Base *Base::create()
{
return new Base();
}
void Base::print()
{
std::cout << a << std::endl;
std::cout << b << std::endl;
}

我的exe中的代码是:

Base *b = Base::create();
std::cout << b->getA() << std::endl;
std::cout << b->getB() << std::endl;
b->print();

通常我应该期望输出:

3.14
1.42
3.14
1.42

但在实践中,我有:

2.122e-314
1.42
3.14
1.42

原因是,对于第一行(2.122e-314(,exe正在地址this + 0处查找a,但由于该内存现在被arr占用,因此与我们所做的一样:std::cout << *((double *)arr)b的值不受影响,因为b总是从DLL中读取的,而getB()不是内联的。

现在,如果我用Base *b = new B();更改Base *b = Base::create();,程序就会崩溃,因为new是从.exe中完成的,它将只分配16个字节,而Base()将访问416个字节中的所有成员变量。

备注

为了这个答案的目的,我做了指针算术,假设增量总是1。事实并非如此。

所以当我写this + 8时,它在C++中被理解为reinterpret_cast<double *>(reinterpret_cast<char *>(this) + 8)