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

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

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

12

Решение

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

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

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

Другие решения

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

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

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

5

Гораздо более простой пример, который отличается ч / б от всех.

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

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

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

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

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

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

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

0
По вопросам рекламы [email protected]