【问题标题】:Initialisation of objects with/without vtable初始化有/没有 vtable 的对象
【发布时间】:2015-03-22 13:01:14
【问题描述】:

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

int size = 10;

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

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

buffer[0] = data;

我现在的问题是初始化具有 vtable 的对象和不具有 vtable 的对象有什么区别。

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

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.

为什么非虚函数会起作用?

是不是因为函数是一个普通的类函数而被静态声明的,因此在我们进行赋值时被复制了?

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

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

也许我只是看了很长时间,让自己感到困惑:)

【问题讨论】:

  • 在第二个例子中,Bbuffer 仍然用T=A 声明吗?
  • 查看两个独立运行。缓冲区初始化如下: T* buffer = (T*) new char[size * sizeof(T)];其中 T 是 A 或 B。
  • 第二次运行时我问的不是这个——缓冲区是如何声明的。
  • 第一个问题很好!
  • 如果buffer是多个不同类型的对象组成的数组,则需要为T ** buffer = new T* [size],并且每个都初始化为nullptr,其中T是一个基类所有可以进入那里的类型(A和B)。像 std::vector> 这样的东西可能会更好。

标签: c++ constructor virtual variable-assignment vtable


【解决方案1】:

您的非虚拟函数“工作”(一个相对术语),因为它们不需要 vtable 查找。在幕后是依赖于实现的,但请考虑执行非虚拟成员所需的内容。

您需要一个函数指针和一个this。后者很明显,但是 fn-ptr 是从哪里来的呢?它只是一个普通的函数调用(期待this,然后是任何提供的参数)。这里没有多态潜力。不需要 vtable 查找意味着编译器可以(并且经常这样做)简单地获取我们认为是对象的地址,推送它,推送任何提供的 args,并将成员函数作为普通的-old-call 调用。编译器知道要调用哪个函数,并且不需要 vtable-intermediary。

在非法指针上调用非静态、非虚拟成员函数时,这种情况并不少见。如果函数是虚拟的,那么您通常(如果幸运的话)会在 调用 时崩溃。如果函数是非虚拟的,你通常会(如果你幸运的话)在函数体的某个地方爆炸,因为它试图访问不存在的成员数据(如果你的非虚拟函数包括 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构造对象。在这样做的过程中,不要忘记您还必须通过手动调用其析构函数销毁该对象(这与它占用的内存无关,因为您正在单独管理它) /em>。

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

【讨论】:

  • 好的,所以无论哪种方式,我都应该尝试确保在为我的池创建非平凡对象时进行新的放置。
  • 是的,对非 POD(普通旧数据)类型使用新的展示位置(老实说,这样做也不会造成真正的伤害)。
  • 我想我一直认为,作业复制了所有数据,而不仅仅是可见数据。 memcpy / memmove 实际上会起作用,而不仅仅是使用默认的复制构造函数/赋值运算符。
  • @mutiju 是的,不要在 C++ 池中玩 C 玩具。一般是个坏主意。试想像memcpy 这样的非平凡成员(例如:std::string)会发生什么。埃加兹。顺便提一句。第一个问题很好。上调了。
【解决方案2】:
new char[...]

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

【讨论】:

  • 是的,我知道,我实际上写了我的问题。我还写了你可以使用 new (buffer[0]) T();为了在创建缓冲区后调用构造函数。但是我的问题仍然存在,在分配过程中会复制什么,如果是所有数据,则应该从原始 no 复制 vtable 指针?
  • 不使用类型系统是问题的根源。
【解决方案3】:

问题不在于虚函数,而更普遍地在于继承。由于缓冲区是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

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

【讨论】:

    猜你喜欢
    • 2013-07-20
    • 1970-01-01
    • 1970-01-01
    • 2020-02-04
    • 2019-05-28
    • 2023-04-08
    • 1970-01-01
    • 2015-02-20
    • 1970-01-01
    相关资源
    最近更新 更多