Подтвердить что ты не робот

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

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

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

Ответ 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

Виртуальная функция-член 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)`
};

Ответ 2

Разница между вызовом функции виртуального участника и вызовом не виртуальной функции-члена заключается в том, что по определению в первом случае целевая функция выбирается в соответствии с динамическим типом выражения объекта, используемого в вызове, тогда как в последнем случае используется статический тип.

Вот и все. Ваш пример наглядно иллюстрирует эту разницу вызовами p2->doA() и p2->doB(). Статический тип выражения *p2 равен Parent, а динамический тип того же выражения - Child. Поэтому p2->doA() вызывает Parent::doA и p2->doB() вызовы Child::doB.

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

Ответ 3

Пример кода, который вы писали в вопросе, по сути дает ответ, когда вы его запускаете.

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

Таким образом, ваш выход программы в этом случае будет:

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

Ответ 4

Более простой пример, который различает b/w все из них.

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

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

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 с использованием разрешения имен, независимо от функции "virtual-ness". Функция

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().