【问题标题】:How does a pointer to a Base class which initialized by a derived class can have *__vptr called the right virtual function?指向由派生类初始化的基类的指针如何使 *__vptr 称为正确的虚函数?
【发布时间】:2020-12-04 15:21:09
【问题描述】:

谁能给我解释一下粗体部分

我不明白为什么 *__vptr 它位于类的 Base 部分,并且 dPtr 可以访问该指针 CAN 突然指向 D1 虚拟表而不是 Base 虚拟表! 我读过一些文章,看了一些资料,但仍然感到困惑。

class Base
{
public:
    virtual void function1() {};
    virtual void function2() {};
};
 
class D1: public Base
{
public:
    virtual void function1() {};
};
 
class D2: public Base
{
public:
    virtual void function2() {};
};
int main()
{
    D1 d1;
    Base *dPtr = &d1;
 
    return 0;
}

注意,因为 dPtr 是一个基指针,它只指向 d1 的基部分。但是,还要注意 *__vptr 位于类的 Base 部分,因此 dPtr 可以访问该指针。 最后,请注意 dPtr->__vptr 指向 D1 虚拟表!因此,即使 dPtr 是 Base 类型,它仍然可以访问D1 的虚拟表(通过 __vptr)。

来源:https://www.learncpp.com/cpp-tutorial/125-the-virtual-table/comment-page-6/#comment-484189

【问题讨论】:

  • vptr 存储在基类中,但每个子类构造函数都将其初始化为指向该特定类的 vtable。
  • 请注意,“为了实现虚函数,C++ 使用了一种特殊形式的后期绑定,称为虚表。”是错的。 C++ 没有虚拟表。 C++ 具有继承、虚函数和覆盖。使用虚拟表是实现它的常用方法,但原则上,如果没有虚拟表,继承、虚拟函数和覆盖的工作方式不会改变,这是一个实现细节
  • ps:我并不是说您的问题不是一个好问题,而是经常将指定的内容与实现细节混淆。如果你想了解虚函数,了解虚表肯定会有所帮助,但不是必须的
  • 但是dPtr 指向D1 对象的Base 部分。这就是 vptr 指向 D1 vtable 的原因。
  • 不,有一个 vptr,D1 ctor 将其设置为指向 D1 vtable,因此在构造之后,甚至指向Base 的指针都指向一个引用的对象D1 vtable。如果还有一个子类,它的构造函数会将 vptr 设置为它的 vtable 等等。

标签: c++ vtable dynamic-dispatch virtual-table


【解决方案1】:

要理解你需要明白C++的实现并不是C++的定义。

C++ 由抽象机器的行为定义。该抽象机的行为由标准定义,兼容的 C++ 编译器必须编译程序以在该抽象机上运行 如同

抽象机器所做的事情的规则是基于真实计算机和 C++ 和 C 程序的真实实现的启发和基础。

因此,当您谈论“虚拟函数表”时,您是在谈论 C++ 对虚拟方法所做的一种常见实现。这种常见的实现没有定义 C++ 的行为方式,将两者混合起来可能会导致问题。


话虽这么说,C++ 的虚拟方法基于在 C 中做基本完全相同的事情。如果您重新实现它,它可以帮助勾勒出虚拟方法和继承在 C++ 中的工作方式。 (这有实际用途,因为这样做可以让您创建自定义对象模型,而自定义对象模型让您可以比 C++ 对象模型更有效地完成某些事情。

struct Bob_vtable {
  void(*print)(Bob const*) = 0;
};

struct Bob {
  Bob_vtable const* vtable = 0;
  int x = 0;

  // glue code to dispatch to vtable:
  void print() const {
    return vtable->print(this);
  }
  
  // implementation of Bob::print:
  static void print_impl( Bob const* self ) {
    std::cout << self->x;
  }

  // vtable helpers:
  static Bob_vtable make_vtable() {
    return { &Bob::print_impl };
  }
  static Bob_vtable const* get_vtable() {
    static const Bob_vtable retval = make_vtable();
    return &retval;
  }
  Bob():vtable(get_vtable()) {}
};

这是一个非常简单的、没有继承的类Bob 的实现,它带有一个虚拟方法print。大致对应:

class Bob {
public:
  int x = 0;
  virtual void print() const { std::cout << x; }
};

您会明白为什么为您编写所有这些胶水代码是件好事。

当你这样做时:

class Alice : public Bob {
public:
  int y = 0;
  void print() const override { std::cout << x << "," << y; }
};

“手动实现”看起来像:

struct Alice : Bob {
  int y = 0;

  // no print glue code needed(!)

  // implementation of Alice::print:
  static void print_impl( Bob const* bobself ) {
    Alice const* self = static_cast<Alice const*>(bobself);
    std::cout << self->x << "," << self->y;
  }

  static Bob_vtable make_vtable() {
    Bob_vtable bob_version = Bob::make_vtable();
    bob_version.print = &Alice::print_impl;
    return bob_version;
  }
  static Bob_vtable const* get_vtable() {
    static const Bob_vtable retval = make_vtable();
    return &retval;
  }

  Alice():Bob() {
    // after constructing Bob, replace the vtable with ours:
    vtable = get_vtable();
  }
};

你有它。

看看这里发生了什么:

Alice a;
a.print(std::cout);

现在,a.print 实际上调用了Bob::print,因为Alice 没有print 方法。

Bob.print 这样做:

  void print() const {
    return vtable->print(this);
  }

它获取该对象实例的vtable指针,并在其中调用打印函数。

Alice 类型对象的 vtable 指针是什么?查看Alice 构造函数。

首先它默认构造Bob(将vtable 设置为指向Bob 的vtable),然后它会这样做:

    vtable = get_vtable();

这个对get_vtable的调用调用Alice::get_vtable

    static const Bob_vtable retval = make_vtable();
    return &retval;

依次调用Alice::make_vtable:

    Bob_vtable bob_version = Bob::make_vtable();
    bob_version.print = &Alice::print_impl;
    return bob_version;

首先调用Bobmake_vtable,然后用Alice::print_impl替换.print

所以Bob::print 调用vtable-&gt;print(this),这是Alice::print_impl(this),它确实:

    Alice const* self = static_cast<Alice const*>(bobself);
    std::cout << self->x << "," << self->y;

虽然此时thisBob const*,但它指向的是Alice 对象,因此static_cast 是有效的。

所以我们从Alice 打印xy

现在,Alice 的 vtable 类型是 Bob_vtable,因为她没有添加任何新方法。如果她添加新方法,她将拥有一个继承自 Bob_vtableAlice_vtable,并且必须通过 static_cast&lt;Alice_vtable const*&gt;(vtable) 访问它们。

这并不完全是“在幕后”所做的,但它在逻辑上与我“即兴发挥”所写的一样。有很多不同的细节,比如vtable中函数的调用约定不同,内存中vtable的格式不匹配等等。


现在,在“手动实现”中,我确实使用了继承。所以那不是C;但是“手动实现”中的继承并没有做任何面向对象的事情。

struct A {int x;}; 
struct B:A{int y;};

只是在做

struct A {
  int x;
}; 
struct B {
  A base;
  int y;
};

顶部有一点句法闪光。

中,“手动实施”与您将如何实施(以及人们这样做)的比例几乎是 1:1。您可以将这些方法移出类,将它们称为void Bob_print(Bob const*) 而不是void Bob::print() const。你会使用struct Alice { Bob base; int y; } 而不是struct Alice:Bob{ int y; };。但区别几乎完全是语法,而不是其他任何东西。

最初开发 时,基于 OO 的 C 语言已经存在,而 C++ 的首要目标之一是能够编写带有类的 C 而无需编写上述所有样板。

现在,C++ 的对象模型不需要上述实现。事实上,依赖上述实现可能会导致程序格式错误或未定义的行为。但是理解实现 C++ 对象模型的一种可能方式有一些用处。另外,一旦你知道如何实现 C++ 的对象模型,你就可以在 C++ 中使用不同的对象模型。

请注意,在现代 C++ 中,我会在上面使用更多模板来删除一些样板。作为一个实际用途,我使用了类似的技术来使用鸭子类型的虚拟方法来实现增强的std::any

结果是你可以得到这个语法:

auto print = poly_method<void(Self const*, std::ostream&)>{
  [](auto const*self, std::ostream& os){ os << *self; }
};
poly_any<&print> x = 7;
x->*print(std::cout);

(不要在家里尝试这个)。

【讨论】:

    猜你喜欢
    • 2015-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-09
    • 2020-04-07
    • 2012-08-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多