Jakie są różnice między nadpisywaniem funkcji wirtualnych a ukrywaniem funkcji nie wirtualnych?

Biorąc pod uwagę następujący fragment kodu, jakie są różnice w wywołaniach funkcji? Czym jest ukrywanie funkcji? Co to jest funkcja nadrzędna? Jak mają one związek z przeciążeniami funkcji? Jaka jest różnica między nimi? Nie mogłem znaleźć dobrego opisu w jednym miejscu, więc pytam tutaj, aby skonsolidować informacje.

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();
}
Author: Jed Schaaf, 2013-11-02

5 answers

Czym jest ukrywanie funkcji?

... jest formą ukrywania imienia. Prosty przykład:

void foo(int);
namespace X
{
    void foo();

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

Dotyczy to również wyszukiwania nazwy w klasie bazowej:

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

Co to jest funkcja nadrzędna?

Jest to związane z pojęciem funkcji wirtualnych. [Klasa.virtual] / 2

Jeśli wirtualna funkcja member vf jest zadeklarowana w klasie Base i w klasie Derived, wywodzącej się bezpośrednio lub pośrednio z Base, to funkcja member vf o tym samym nazwa, parametr-type-list, CV-qualification i ref-qualifier (lub brak tego samego) jak Base::vf jest zadeklarowana, wtedy Derived::vf jest również wirtualny (niezależnie od tego, czy jest tak zadeklarowana) i nadpisuje 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)
};

Ostateczne nadpisanie staje się istotne podczas wywoływania funkcji wirtualnej: [class.virtual] / 2

Wirtualna funkcja member C::vf obiektu klasy S jest ostatecznym nadrzędnym, chyba że najbardziej pochodna klasa, z której S jest podrzędnym obiektem klasy bazowej (jeśli any) deklaruje lub dziedziczy inną funkcję członkowską, która nadpisuje vf.

Tzn. jeśli masz obiekt typu S, ostateczne nadpisanie jest pierwszym nadpisaniem, które widzisz podczas przechodzenia hierarchii klas S z powrotem do klas bazowych. Ważne jest to, że Typ dynamiczny wyrażenia wywołania funkcji jest używany w celu określenia ostatecznego nadpisania:

Base* p = new Derived;
p -> vf();    // dynamic type of `*p` is `Derived`

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

Jaka jest różnica między nadrzędnym a ukrywasz się?

Zasadniczo funkcje w klasie bazowej są zawsze ukryte przez funkcje o tej samej nazwie w klasie pochodnej; bez względu na to, czy funkcja w klasie pochodnej nadpisuje wirtualną funkcję klasy bazowej, czy nie:

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

Aby znaleźć nazwę funkcji, używany jest statyczny typ wyrażenia:

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

Jak mają się one do przeciążeń funkcji?

Jako że "ukrywanie funkcji" jest formą ukrywania nazw, wszystkie przeciążenia mają wpływ, jeśli nazwa funkcji jest ukryty:

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

Dla nadpisywania funkcji, tylko funkcja w klasie bazowej z tymi samymi argumentami będzie nadpisywana; można oczywiście przeciążać funkcję wirtualną:

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)`
};
 18
Author: dyp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-04 20:19:16

Różnica między wywołaniem wirtualnej funkcji member a wywołaniem nie-wirtualnej funkcji member jest taka, że w pierwszym przypadku funkcja docelowa jest wybierana zgodnie z typem dynamic wyrażenia obiektu użytego w wywołaniu, podczas gdy w drugim przypadku używany jest typ statyczny.

To wszystko. Twój przykład wyraźnie ilustruje tę różnicę wywołaniami p2->doA() i p2->doB(). Typ statyczny *p2 wyrażenie to Parent, natomiast typ dynamiczny tego samego wyrażenia to Child. Dlatego p2->doA() wywołuje Parent::doA i p2->doB() wywołuje Child::doB.

W kontekstach, w których ta różnica ma znaczenie, ukrywanie nazw w ogóle nie pojawia się w obrazie.

 5
Author: AnT,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-21 22:51:57

O wiele łatwiejszy przykład, który różni b / w wszystkie z nich.

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
}
 2
Author: AbhimanyuAryan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-27 22:15:18

Przykładowy kod, który piszesz w pytaniu, zasadniczo daje odpowiedź podczas jego uruchamiania.

Wywołanie funkcji nie-wirtualnej użyje funkcji z tej samej klasy co typ wskaźnika, niezależnie od tego, czy obiekt został faktycznie utworzony jako inny typ Pochodny. Podczas gdy wywołanie funkcji wirtualnej użyje funkcji z oryginalnego przydzielonego typu obiektu, niezależnie od tego, jakiego rodzaju wskaźnika używasz.

Więc wyjście programu w tym przypadku będzie be:

doA in Parent
doA in Parent
doA in Child
doB in Parent
doB in Child
doB in Child
 1
Author: karadoc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-01 23:30:11

Zaczniemy od tych Å‚atwych.

p1 jest wskaźnikiem Parent, więc zawsze będzie wywoływał funkcje Członkowskie Parent.

cp jest wskaźnikiem do Child, więc zawsze wywoła funkcje Członkowskie Child.

Teraz ten trudniejszy. p2 jest wskaźnikiem Parent, ale wskazuje na obiekt typu Child, więc wywoła funkcje Child, gdy dopasowana Funkcja Parent jest wirtualna lub funkcja istnieje tylko w Child, a nie w Parent. Innymi słowy, Child ukrywa Parent::doA() z własnym doA(), ale nadpisuje Parent::doB(). Ukrywanie funkcji jest czasami uważane za formę przeciążenia funkcji, ponieważ funkcja o tej samej nazwie ma inną implementację. Ponieważ funkcja ukrywania jest w innej klasie niż funkcja ukryta, ma inną sygnaturę, która jasno określa, której użyć.

Wyjście dla testStuff() będzie

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

W każdym przypadku, Parent::doA() i Parent::doB() mogą być wywoływane w obrębie {[7] } za pomocą rozdzielczości nazw, niezależnie od funkcji "wirtualnej". Funkcja

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

Pokazuje to, gdy wywołane przez cp->doX() przez wyjście

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

Dodatkowo, cp->Parent::doA() wywoła Parent's version of doA().

p2 nie może odnosić się do doX(), ponieważ jest to Parent*, a Parent nie wie o niczym w Child. Jednak p2 może być przypisany do Child*, ponieważ został zainicjowany jako jeden, a następnie może być użyty do wywołania doX().

 0
Author: Jed Schaaf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-08 11:57:55