初始化对象 with/without vtable

Initialisation of objects with/without vtable

假设我有一个分配一些缓冲区的池。

int size = 10;

T* buffer = (T*) new char[size * sizeof(T)];

如果我现在想将一些数据分配给缓冲区,我会执行以下操作。

buffer[0] = data;

我现在的问题是,具有 vtable 和不具有 vtable 的对象在初始化方面有何不同。

据我所知,我可以毫无问题地将 classes 分配给这个缓冲区,只要我不调用任何虚函数,函数调用就可以正常工作。 例如

class A{
    void function(){}
};

A a;
buffer[0] = a;
a.function(); // works

但是:

class B{
    void function(){}
    virtual void virtual_function(){}
};

B b;
buffer[0] = b;
b.function(); // does work
b.virtual_function() // does not work.

为什么非虚函数有效?

是否因为该函数是静态声明的,因为它是一个普通的class函数,因此在我们进行赋值时被复制?

但是我需要在我创建的缓冲区上调用构造函数以防万一我需要确保虚函数也能正常工作是没有意义的。 new (buffer[0]) T(); 以便在创建的对象上调用构造函数。

这两个示例首先创建适当大小的缓冲区,然后进行分配,将其视为一个池,我根据我想要放入池中的对象数量预先分配内存。

也许我只是看了很久,把自己搞糊涂了:)

new char[...]

这不构造对象 T(不调用构造函数)。 虚拟 table 是在构建过程中创建的。

你的非虚拟函数 "work"(一个相对的术语)因为它们不需要查找 vtable。引擎盖下是依赖于实现的,但考虑执行非虚拟成员需要什么。

你需要一个函数指针,还有一个this。后者是显而易见的,但 fn-ptr 从何而来?它只是一个普通的函数调用(期望 this,然后是任何提供的参数)。这里没有多态潜力。不需要 vtable 查找意味着编译器可以(并且经常)简单地获取我们认为是对象的地址,压入它,压入任何提供的 args,并将成员函数作为普通的旧 call 调用。编译器知道调用哪个函数,不需要 vtable 中介。

在非法指针上调用非静态、非虚拟成员函数时,这会引起头痛,这种情况并不少见。如果该函数是虚函数,您通常(如果幸运的话)会在 调用 时崩溃。如果函数是非虚拟的,你通常会(如果你幸运的话)在函数体的某个地方爆炸,因为它试图访问不存在的成员数据(如果你的非虚函数,包括 vtable-directed 执行-virtual 调用虚拟)。

为了证明这一点,请考虑这个(显然是 UB)示例。试试吧。

#include <iostream>

class NullClass
{
public:
    void call_me()
    {
        std::cout << static_cast<void*>(this) << '\n';
        std::cout << "How did I get *here* ???" << '\n';
    }
};

int main()
{
    NullClass *noObject = NULL;
    noObject->call_me();
}

输出 (OSX 10.10.1 x64, clang 3.5)

0x0
How did I get *here* ???

最重要的是,当您分配原始内存并按原样通过强制转换分配指针时,没有 vtable 绑定到对象。如果你想这样做,你需要通过placement-new构造对象。在这样做的时候,不要忘记你还必须 destroy 对象(这与它占用的内存无关,因为你单独管理它)通过调用它的析构函数 手动.

最后,您正在调用的赋值不会复制 vtable。坦率地说,没有理由这样做。正确构造的对象的 vtable 已经正确构建,并由给定对象实例的 vtable 指针引用。 Said-pointer 参与对象复制,它有自己的一套语言标准强制要求。

问题不特别出在虚函数上,而是更普遍地出在继承上。由于 buffer 是 A 的数组,当你写 :

B b;
buffer[0] = b;

你首先构建了一个B对象(第一行),然后使用其用b初始化的复制构造函数构建了一个A对象(第二行)。

因此,当您稍后调用 buffer[0].virtual_function() 时,实际上是将虚函数应用于 A 对象,而不是 B 对象。

顺便说一句,直接调用 b.virtual_function() 应该仍能正确调用 B 版本,因为它应用于真实的 B 对象:

B b;
buffer[0] = b;
b.virtual_function(); // calls B version

如果不需要复制对象,可以使用指针数组。