在共享库中添加成员变量到class,不会破坏二进制兼容性吗?

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

我本来想找个例子来表达一下我对二进制兼容性的理解,结果搞砸了。我想通过在class的开头或中间添加成员来改变class的成员在DLL中的布局,并期望无法正确访问该变量 或者访问变量会生成crash.However,一切顺利。我发现,无论我如何将成员变量添加到class的任何位置,都没有崩溃,也没有破坏二进制兼容性。我的代码如下:

//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.I中的实现和声明是分开的,试图直接访问变量和通过函数访问变量(如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();
}

其中,classBaseDerived是从dll导出的。 无论我如何将成员变量添加到 BaseDerived 任何地方,都没有崩溃,也没有破坏二进制兼容性。

我正在使用 qt.There 是 在这里,但对我没有帮助。

此外,我删除了DLL中class的所有成员var,我仍然通过链接DLL在客户端中使用不存在的变量,赋值,获取它......似乎足够space保留在动态库中被客户端重新定义,即使没有成员变量也是defined.So奇怪!

我的问题是,为什么改变DLL中class成员的布局,不会破坏二进制兼容性?删除DLL中class的所有成员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 像这样:

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 的定义同时更新 class.

您可以通过内嵌 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] 后,aven 您的 .exe 仍将尝试访问现在被 arr.

占用的 *(this + 0)

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

如果你反其道而行之:

  1. arr
  2. 编译你的 exe
  3. 删除arr
  4. 构建 DLL
  5. 运行.exe

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


这是一个例子。

这是 .exe 的 header:

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

DLLheader

class基地 { public: 根据(); 双 getA() const { return a; } 双 getB() 常量; 静态基础*创建(); 无效打印(); 私人的: 诠释[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,但是由于这个内存现在被[=35=占用了] 就像我们会做的那样:std::cout << *((double *)arr)b 的值不受影响,因为 b 始终从 DLL 中读取,因为 getB() 未内联。

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

备注

为了这个答案的目的,我做了指针运算,假设增量总是 1。实际上它不是。

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