覆盖虚函数和隐藏非虚函数有什么区别

What are the differences between overriding virtual functions and hiding non-virtual functions?

本文关键字:函数 什么 区别 隐藏 覆盖      更新时间:2023-10-16

给定以下代码片段,函数调用有什么区别?什么是函数隐藏?什么是函数覆盖?它们与函数重载有何关系?两者有什么区别?我无法在一个地方找到对这些的良好描述,所以我在这里询问,以便我可以整合信息。

class Parent {
  public:
    void doA() { cout << "doA in Parent" << endl; }
    virtual void doB() { cout << "doB in Parent" << endl; }
};
class Child : public Parent {
  public:
    void doA() { cout << "doA in Child" << endl; }
    void doB() { cout << "doB in Child" << endl; }
};
Parent* p1 = new Parent();
Parent* p2 = new Child();
Child* cp = new Child();
void testStuff() {
  p1->doA();
  p2->doA();
  cp->doA();
  p1->doB();
  p2->doB();
  cp->doB();
}

什么是函数隐藏?

。是名称隐藏的一种形式。一个简单的例子:

void foo(int);
namespace X
{
    void foo();
    
    void bar()
    {
        foo(42); // will not find `::foo`
        // because `X::foo` hides it
    }
}

这也适用于基类中的名称查找:

class Base
{
public:
    void foo(int);
};
class Derived : public Base
{
public:
    void foo();
    void bar()
    {
        foo(42); // will not find `Base::foo`
        // because `Derived::foo` hides it
    }
};
<小时 />

什么是函数覆盖?

这与虚拟功能的概念有关。[类.虚拟]/2

如果一个虚成员函数vf是在类Base和类Derived中声明的,直接或间接派生自Base,则成员函数vf具有与声明Base::vf相同的名称、参数类型列表、cv-限定符和ref限定符(或不存在相同),则Derived::vf也是虚拟的(无论它是否如此声明),并且它覆盖Base::vf

class Base
{
private:
    virtual void vf(int) const &&;
    virtual void vf2(int);
    virtual Base* vf3(int);
};
class Derived : public Base
{
public: // accessibility doesn't matter!
    void vf(int) const &&; // overrides `Base::vf(int) const &&`
    void vf2(/*int*/);     // does NOT override `Base::vf2`
    Derived* vf3(int);     // DOES override `Base::vf3` (covariant return type)
};

调用虚函数时,最终覆盖器变得相关:[class.virtual]/2

类对象的虚拟成员函数C::vf S是最终覆盖程序,除非派生最多的类(其中S是基类子对象(如果有)声明或继承另一个重写vf的成员函数。

也就是说,如果你有一个类型S的对象,最终的覆盖器是你在遍历S类层次结构回到其基类时看到的第一个覆盖器。重要的一点是,函数调用表达式的动态类型用于确定最终覆盖器:

Base* p = new Derived;
p -> vf(42);    // dynamic type of `*p` is `Derived`
Base& b = *p;
b  . vf(42);    // dynamic type of `b` is `Derived`
<小时 />

覆盖和隐藏有什么区别?

本质上,基类中的函数总是被派生类

中同名的函数隐藏;无论派生类中的函数是否覆盖基类的虚函数:

class Base
{
private:
    virtual void vf(int);
    virtual void vf2(int);
};
class Derived : public Base
{
public:
    void vf();     // doesn't override, but hides `Base::vf(int)`
    void vf2(int); // overrides and hides `Base::vf2(int)`
};

要查找函数名称,请使用表达式的静态类型:

Derived d;
d.vf(42);   // `vf` is found as `Derived::vf()`, this call is ill-formed
            // (too many arguments)
<小时 />

它们与函数重载有何关系?

由于"函数隐藏"是名称隐藏的一种形式,如果隐藏函数的名称,则所有重载都会受到影响:

class Base
{
private:
    virtual void vf(int);
    virtual void vf(double);
};
class Derived : public Base
{
public:
    void vf();     // hides `Base::vf(int)` and `Base::vf(double)`
};

对于函数重写,只有基类中具有相同参数的函数才会被覆盖;当然,您可以重载虚函数:

class Base
{
private:
    virtual void vf(int);
    virtual void vf(double);
    void vf(char);  // will be hidden by overrides in a derived class
};
class Derived : public Base
{
public:
    void vf(int);    // overrides `Base::vf(int)`
    void vf(double); // overrides `Base::vf(double)`
};

调用虚拟成员函数和调用非虚拟成员函数之间的区别在于,根据定义,在前一种情况下,目标函数是根据调用中使用的对象表达式的动态类型选择的,而在后一种情况下,使用静态类型。

仅此而已。您的示例通过p2->doA()p2->doB()调用清楚地说明了这种差异。*p2表达式的静态类型为Parent,而同一表达式的动态类型为Child。这就是为什么p2->doA()打电话Parent::doAp2->doB()打电话Child::doB

在这种差异很重要的情况下,名称隐藏根本不会出现。

我们将从简单的开始。

p1是一个Parent指针,所以它将始终调用Parent的成员函数。

cp是一个指向Child的指针,所以它将永远调用Child的成员函数。

现在更难的那个。 p2是一个Parent指针,但它指向一个类型为 Child 的对象,所以只要匹配的函数Parent是虚拟的,或者函数只存在于Child中而不存在于Parent中,它就会调用Child的函数。换句话说,Child用自己的doA()隐藏Parent::doA(),但它覆盖了Parent::doB()。函数隐藏有时被认为是函数重载的一种形式,因为具有相同名称的函数被赋予了不同的实现。由于隐藏函数与隐藏函数位于不同的类中,因此它确实具有不同的签名,这清楚地表明要使用哪个签名。

testStuff()的输出将是

doA in Parent
doA in Parent
doA in Child
doB in Parent
doB in Child
doB in Child

在任何情况下,都可以使用名称解析在Child内调用Parent::doA()Parent::doB(),而不管函数的"虚拟性"如何。函数

void Child::doX() {
  doA();
  doB();
  Parent::doA();
  Parent::doB();
  cout << "doX in Child" << endl;
}

当由输出cp->doX()调用时演示了这一点

doA in Child
doB in Child
doA in Parent
doB in Parent
doX in Child

此外,cp->Parent::doA()将调用Parent版本的doA()

p2不能指doX(),因为它是Parent*ParentChild一无所知。但是,p2可以强制转换为Child*,因为它被初始化为一个,然后可以用来调用doX()

一个更简单的例子,与所有这些黑白不同。

class Base {
public:
    virtual int fcn();
};
class D1 : public Base {
public:  
    // D1 inherits the definition of Base::fcn()
    int fcn(int);  // parameter list differs from fcn in Base
    virtual void f2(); // new virtual function that does not exist in Base
};
class D2 : public D1 {
public:
    int fcn(int); // nonvirtual function hides D1::fcn(int)
    int fcn();  // overrides virtual fcn from Base
    void f2();  // overrides virtual f2 from D1
}

您在问题中编写的示例代码在运行时基本上给出了答案。

调用非虚函数将使用与指针类型相同的类中的函数,而不管该对象是否实际上是作为其他派生类型创建的。而调用虚拟函数将使用原始分配对象类型的函数,而不管您使用哪种指针。

因此,在这种情况下,程序的输出将是:

doA in Parent
doA in Parent
doA in Child
doB in Parent
doB in Child
doB in Child