【问题标题】:Base-class pointer pointing to a derived class cannot access derived-class method指向派生类的基类指针不能访问派生类方法
【发布时间】:2020-02-11 15:29:16
【问题描述】:

我正在学习C++11的继承,我发现如果派生类重新定义了一个虚函数名但原型不同,则分配了指向派生类的指针的基类指针只能访问函数的基类版本。无法访问派生版本功能。我想知道为什么会这样。

class Enemy {
public:
  virtual void describe() { std::cout << "Enemy"; }
};
class Dragon : public Enemy {
public:
  virtual void describe(int dummy) { std::cout << "Dragon"; }
};

main

Dragon foo;
Enemy* pe = &foo;
pe->describe(); // Enemy
foo.describe(1); // Dragon
pe->describe(1); // no matching function, candidate is Enemy::describe()

根据我对虚函数表的了解,pe 指向的派生对象(即foo)应该有一个指向Dragon 的vtable 的vpointer 成员。我也知道在派生类中重新定义函数名会隐藏基类中的所有同名函数。所以在 Dragon 的 vtable 中,describe 的地址应该是带有参数int dummy 的函数。

但事实证明pe 可以访问Enemy 的方法版本,应该是隐藏的。而pe 无法访问Dragon 的方法版本,该方法应该在pe 的vtable 中。它的执行就像使用了Enemy 的vtable。为什么会这样?

更新: 我想现在我或多或少地了解了它背后的机制。这是我的假设:

由于是指向Enemy的指针,所以程序会先在Enemy的作用域内查找方法名。如果未找到该名称,编译器将给出错误。如果它不是虚拟的,则调用它。如果是虚拟的,则在Enemy的vtable中记录方法的偏移量。然后程序使用这个偏移量来访问目标对象的vtable中正确的方法。

如果该方法被正确覆盖,目标对象的 vtable 中该偏移量处的函数地址将被更改。否则,它将与 Enemy 的 vtable 中的函数地址相同,如示例中所示。

由于Dragondescribeint dummy是不同的原型,它被添加到Dragon的vtable中,在原来的describe继承自Enemy之后。但是int dummy 版本无法从Enemy* 访问,因为Enemy 的vtable 甚至没有那个偏移量。

这是正确的吗?

【问题讨论】:

  • pe 正在使用Enemy API,并且被访问的方法不是该 API 的一部分。
  • 我已经更新了我对示例中发生的情况的假设。

标签: c++ c++11 virtual-functions dynamic-binding method-hiding


【解决方案1】:

同名但签名不同的函数本质上是不同的函数。

通过在Dragon 类中声明virtual void describe(int dummy),您已经声明了一个新的虚函数,而不是覆盖原来的虚函数(Enemy 中的virtual void describe())。您只能覆盖具有相同签名的虚函数。

您不能在指向Enemy 的指针上调用describe(1),因为c++ 根据实例的编译时类型调用函数(尽管可以动态分派这样的调用以调用实际的覆盖方法)。

【讨论】:

  • 但是为什么这里的函数是在编译时而不是运行时调度的呢?据我了解,如果函数在运行时调度,程序应该能够在Dragon的vtable中找到describe,尽管它与Enemy中的describe无关。这是真的吗?
  • @Elucidase 函数存在于 vtable 中,但无法从指向 Enemy 的指针访问,因为 Enemy 没有该成员函数(它有一个同名的成员函数,但不是同一个成员函数)。
  • @Elucidase 另外,从编译器的角度思考。如果你有另一种类型,比如Soldier,它也派生自Enemy,它具有describe 方法,但带有签名describe()(没有int)。你给编译器一个指向Enemy 的指针,然后在它上面调用describe(1)。编译器如何知道编译器是 Dragon 类型还是 Soldier 类型?虚拟方法是为函数调用创建solid接口的好方法,而不是即兴发挥各种参数。
  • 但是当函数被正确覆盖时,程序可以从Enemy*指针访问Dragon的vtable。而且从我了解到的情况来看,似乎 vtable 和动态调度正是处理编译时类型未知的情况的机制。
  • @Elucidase 您的建议在理论上是可能的(证据:c++ 中有dynamic_cast),但在您的上下文中根本不允许使用它们。
【解决方案2】:

在 C++ 中,同名但参数不同的函数是完全独立的函数,彼此无关。他们同名的事实完全无关紧要。

这与调用基类“apple”中的函数和派生类“banana”中的函数完全相同。由于基类中没有“香蕉”函数,因此显然不能在基类中调用它。派生类中的香蕉函数显然不会覆盖基类中的函数。

我也知道在派生类中重新定义函数名 将隐藏基类中的所有同名函数。

这是不正确的。只有当它具有相同的名称和相同的参数(以及任何限定符,如果有或没有任何限定符)时,它才会隐藏它。

【讨论】:

    【解决方案3】:

    事实上你有:

    class Enemy {
    public:
      virtual void describe() { std::cout << "Enemy"; }
    };
    
    class Dragon : public Enemy {
    public:
      // void describe() override { Enemy::describe(); } // Hidden
      virtual void describe(int dummy) { std::cout << "Dragon"; }
    };
    

    重载方法的选择是静态完成的:

    • Enemy 上的指针/引用仅见 void Enemy::describe()

    • Dragon 上的指针/引用只能看到 void Dragon::describe(int)(但可以显式访问 void Enemy::describe())。

    然后以运行时类型完成虚拟调度。

    所以

    Dragon foo;
    Enemy* pe = &foo;
    
    foo.describe();         // KO: Enemy::describe() not visible (1)
    foo.Enemy::describe();  // OK: Enemy::describe()
    foo.describe(1);        // OK: Dragon::describe(int)
    
    pe->describe();         // OK: Enemy::describe()
    pe->describe(1);        // KO: No Enemy::describe(int)
    pe->Dragon::describe(1);// KO: Dragon is not a base class of Enemy
    

    (1) 可以通过更改Dragon 来修复

    class Dragon : public Enemy {
    public:
      using Enemy::describe; // Unhide Enemy::describe()
    
      virtual void describe(int dummy) { std::cout << "Dragon"; }
    };
    

    【讨论】:

    • 但是为什么pe-&gt;describe 是静态绑定(调​​度)而不是动态绑定呢?我假设在运行时,当程序试图调度一个虚方法时,它会查看对象的虚函数表。在这种情况下,它是 Dragon 的 vtable。
    • void (C::*)describe()void (C::*)describe(int) 是 2 个不同的签名。 Dragon vtable 将有 2 个条目,一个用于 Enemy::describe()(它不会覆盖,因此与 Enemy 实例具有相同的值),一个用于 Dragon::describe(int)
    猜你喜欢
    • 2016-07-03
    • 1970-01-01
    • 2014-06-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-16
    • 2022-01-03
    • 1970-01-01
    相关资源
    最近更新 更多