【问题标题】:What are the differences between overriding virtual functions and hiding non-virtual functions?覆盖虚函数和隐藏非虚函数有什么区别?
【发布时间】:2013-11-13 05:10:41
【问题描述】:

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

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();
}

【问题讨论】:

  • 这闻起来像家庭作业或类似的东西。我不知道你为什么要问一个如此广泛的问题。
  • @kerrek-sb 我在网上找不到比较这些的解释(这是我通常看的地方,其他人可能也会这样做)。他们有一个或另一个的解释,但不是两者都有,我想把一些容易参考的东西放到网上。感谢您对访问说明符的了解。
  • @karadoc 不,这只是我发现的东西。我找到的解释解释了一个或另一个,但不要对它们进行比较,这就是我想要的。
  • @karadoc,FWIW,我正要发布同样的问题,不,这不是家庭作业。我很久没上大学了。感谢您提出这个 Jed,尽管不幸的是我仍然不明白 virtual 关键字的用途,除非您尝试使用纯虚函数创建抽象基类。除此之外,从实际的角度来看,我看不到“虚拟”的作用,因为我只能隐藏该功能。
  • 如果这听起来像是一项家庭作业,那就没问题了。我没有做任何作业,这个确切的问题出现在我的脑海中,我用谷歌搜索,发现了这个很棒的资源。我想许多其他人也处于同样的境地。立即找到答案非常有用。

标签: c++ inheritance virtual-functions overriding method-hiding


【解决方案1】:

什么是函数隐藏?

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

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
    }
};

什么是函数覆盖?

这与虚函数的概念有关。 [class.virtual]/2

如果一个虚成员函数vf在类Base和类Derived中声明,直接或间接派生自Base,则成员函数vf同名,参数类型-list、cv-qualification 和 ref-qualifier(或不存在相同)作为 Base::vf 已声明,然后 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

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

即如果您有一个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)`
};

【讨论】:

  • 谢谢!您对此的解释比我自己的回答更清楚、更清楚。
  • @JedSchaaf 我确实在 cmets 中包含返回类型故意:只有名称与名称隐藏相关,只有名称加上参数与覆盖有关。如果返回类型与被覆盖函数的返回类型不同或不协变,则返回类型只能使覆盖非法。
  • 不过,在同一行,vf2(int) 覆盖并隐藏了 vf(int)。编辑必须至少有 6 个字符,所以我添加了一些我认为可能无关紧要的内容。抱歉不清楚。 ://
  • @JedSchaaf 哦,谢谢!这实际上是一个错字。是的.. SO 的编辑系统不鼓励(或将不鼓励)这种小的编辑,因为每个编辑都必须由几个人审查。请参阅 Meta Stack Overflow 上的 this discussion
  • 另外值得注意的是,如果非虚方法Base::foo()在基类中调用虚方法Base::vf(),那么从派生类调用foo()会触发覆盖方法@987654348 @,而不是原来的Base::vf()
【解决方案2】:

调用 virtual 成员函数和调用 non-virtual 成员函数的区别在于,根据定义,在前一种情况下,目标函数是根据在调用中使用对象表达式的 dynamic 类型,而在后一种情况下使用 static 类型。

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

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

【讨论】:

  • 谢谢,静态动态片正是我要找的;说得通。因此,函数隐藏在速度方面会更好,只要可以使用它,但如果使用基类类型,则通常情况并非如此。
  • 第一段总结了任何可能的答案和任何 100 个例子。谢谢!
【解决方案3】:

我们将从简单的开始。

p1 是一个Parent 指针,所以它总是会调用Parent 的成员函数。

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

现在难度更大了。 p2 是一个Parent 指针,但它指向一个Child 类型的对象,所以只要匹配的Parent 函数是虚函数或者该函数只存在于@987654332 中,它就会调用Child 的函数@ 而不是Parent。换句话说,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

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

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

cp-&gt;doX() 调用时通过输出证明这一点

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

另外,cp-&gt;Parent::doA() 将调用ParentdoA() 版本。

p2 不能引用doX(),因为它是Parent*,而Parent 不知道Child 中的任何内容。但是,p2 可以强制转换为Child*,因为它被初始化为 1,然后它可以用来调用doX()

【讨论】:

  • @ChristopherStevenson 可能是因为提出这个问题的人是同一个人。所以看起来他问这个问题只是为了获得一些声望点,而不是真正学习一些东西。
  • 你为什么认为我没有学过这个?我有一段时间没有看这些主题了,所以我决定查一下。当我无法快速找到我正在寻找的确切内容时,我决定将其添加到此处,以供可能有类似问题的其他人参考。你不喜欢我回答我自己的问题吗?问题提交可以选择立即添加答案,以“分享您的知识,问答风格。”
【解决方案4】:

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

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
}

【讨论】:

    【解决方案5】:

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

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

    所以你的程序在这种情况下的输出将是:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-06-01
      • 2011-11-23
      • 2011-06-16
      • 2015-04-20
      • 1970-01-01
      • 1970-01-01
      • 2013-10-04
      • 2011-07-28
      相关资源
      最近更新 更多