Каковы различия между переопределением виртуальных функций и сокрытием не виртуальных функций?

Учитывая следующий фрагмент кода, каковы различия в вызовах функций? Что такое скрытие функций? Что такое переопределение функции? Как они связаны с перегрузками функций? Какая разница между двумя? Я не мог найти хорошее описание их в одном месте, поэтому я спрашиваю здесь, чтобы я мог объединить информацию.

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

5 ответов

Решение

Что такое скрытие функций?

... это форма сокрытия имени. Простой пример:

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 с тем же именем, параметром-списком-типом, cv-квалификацией и ref-квалификатором (или отсутствием того же), что и 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

Виртуальная функция-член C::vf объекта класса S является окончательным переопределением, если самый производный класс которого S подобъект базового класса (если есть) объявляет или наследует другую функцию-член, которая переопределяет vf,

Т.е. если у вас есть объект типа Sокончательный переопределение - это первое переопределение, которое вы видите при обходе иерархии классов S вернуться к своим базовым классам. Важным моментом является то, что динамический тип выражения вызова функции используется для определения окончательного переопределения:

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

Base& b = *p;
b  . vf();    // 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::doA а также p2->doB() звонки Child::doB,

В тех случаях, когда это различие имеет значение, сокрытие имени вообще не входит в картину.

Начнем с простых.

p1 это Parent указатель, поэтому он всегда будет вызывать Parentфункции-члены.

cp это указатель на Childтак будет всегда звонить Childфункции-члены.

Теперь более сложный. p2 это Parent указатель, но он указывает на объект типа Childтак оно и будет звонить Childфункции всякий раз, когда сопоставление Parent функция является виртуальной или функция существует только внутри Child и не в Parent, Другими словами, Child шкуры Parent::doA() со своим 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->doX() путем вывода

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

Дополнительно, cp->Parent::doA() позвоню Parentверсия doA(),

p2 не может ссылаться на doX() потому что это Parent*, а также Parent ничего не знает в Child, Тем не мение, 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
Другие вопросы по тегам