关于c ++:重写虚函数和隐藏非虚函数有什么区别?

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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();
}


什么是函数隐藏?

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

1
2
3
4
5
6
7
8
9
10
11
void foo(int);
namespace X
{
    void foo();

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

If a virtual member function vf is declared in a class Base and in a class Derived, derived directly or indirectly from Base, a member function vf with the same name, parameter-type-list, cv-qualification, and ref-qualifier (or absence of same) as Base::vf is declared, then Derived::vf is also virtual (whether or not it is so declared) and it overrides Base::vf.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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时,最终的重写器变得相关。

A virtual member function C::vf of a class object S is a final overrider unless the most derived class of which S is a base class subobject (if any) declares or inherits another member function that overrides vf.

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

1
2
3
4
5
Base* p = new Derived;
p -> vf();    // dynamic type of `*p` is `Derived`

Base& b = *p;
b  . vf();    // dynamic type of `b` is `Derived`

覆盖和隐藏有什么区别?

基本上,基类中的函数总是被派生类中同名的函数隐藏;无论派生类中的函数是否重写基类的虚函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)`
};

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

1
2
3
Derived d;
d.vf(42);   // `vf` is found as `Derived::vf()`, this call is ill-formed
            // (too many arguments)

它们如何与函数重载相关?

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

1
2
3
4
5
6
7
8
9
10
11
12
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)`
};

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

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


一个更简单的例子,它不同于所有的b/w。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
}

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

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

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

1
2
3
4
5
6
doA in Parent
doA in Parent
doA in Child
doB in Parent
doB in Child
doB in Child


我们从简单的开始。

p1Parent指针,所以它总是调用Parent的成员函数。

cp是指向Child的指针,因此它总是调用Child的成员函数。

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

testStuff()的输出为

1
2
3
4
5
6
doA in Parent
doA in Parent
doA in Child
doB in Parent
doB in Child
doB in Child

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

1
2
3
4
5
6
7
void Child::doX() {
  doA();
  doB();
  Parent::doA();
  Parent::doB();
  cout <<"doX in Child" << endl;
}

cp->doX()通过输出调用时演示此问题

1
2
3
4
5
doA in Child
doB in Child
doA in Parent
doB in Parent
doX in Child

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

p2不能指doX(),因为它是Parent*Parent不知道Child中的任何内容。但是,由于p2初始化为一个Child*,因此可以将它强制转换为Child*,然后可以使用它来调用doX()